volatile引出的單例模式之雙重檢查

2021-10-06 05:10:29 字數 2174 閱讀 5744

保證執行緒可見性(mesi,利用cpu的快取一致性協議)

禁止指令重排序(cpu)

在實現單例模式時,如果未考慮多執行緒的情況,就容易寫出下面的錯誤**:

public

class

singleton

public singleton getinstance()

return uniquesingleton;

}}

在多執行緒的情況下,這樣寫可能會導致uniquesingleton有多個例項。比如下面這種情況,考慮有兩個執行緒同時呼叫getinstance():

time

thread-a

thread-b

t1檢查到uniquesingleton為空

t2檢查到uniquesingleton為空

t3初始化物件a

t4返回物件a

t5初始化物件b

t6返回物件b

可以看到,uniquesingleton被例項化了兩次並且被不同物件持有。完全違背了單例的初衷。

出現上面這種情況,第一反應就是加鎖,如下:

public

class

singleton

public synchronized singleton getinstance()

return uniquesingleton;

}}

這樣雖然解決了問題,但是因為用到了synchronized,會導致很大的效能開銷,並且加鎖其實只需要在第一次初始化的時候用到,大部分情況下都已經被初始化,之後的呼叫都沒必要再進行加鎖。

雙重檢查鎖(double checked locking)是對上述問題的一種優化。先判斷物件是否已經被初始化,再決定要不要加鎖。

public

class

singleton

public singleton getinstance()

}}return uniquesingleton;

}}

如果這樣寫,執行順序就成了:

執行雙重檢查是因為,如果多個執行緒同時了通過了第一次檢查,並且其中乙個執行緒首先通過了第二次檢查並例項化了物件,那麼剩餘通過了第一次檢查的執行緒就不會再去例項化物件。

這樣,除了初始化的時候會出現加鎖的情況,後續的所有呼叫都會避免加鎖而直接返回,解決了效能消耗的問題。在高併發的情況下這樣的作法是會出現問題的:

上述寫法看似解決了問題,但是有個很大的隱患。例項化物件的那行**(標記為error的那行),實際上可以分解成以下三個步驟:

分配記憶體空間

初始化物件

將物件指向剛分配的記憶體空間

但是有些編譯器為了效能的原因,可能會將第二步和第三步進行重排序,順序就成了:

分配記憶體空間

將物件指向剛分配的記憶體空間

初始化物件

現在考慮重排序後,兩個執行緒發生了以下呼叫:

time

thread a

thread b

t1檢查到uniquesingleton為空

t2獲取鎖

t3再次檢查到uniquesingleton為空

t4為uniquesingleton分配記憶體空間

t5將uniquesingleton指向記憶體空間

t6檢查到uniquesingleton不為空

t7訪問uniquesingleton(此時物件還未完成初始化)

t8初始化uniquesingleton

在這種情況下,t7時刻執行緒b對uniquesingleton的訪問,訪問的是乙個初始化未完成的物件, b拿到的是乙個沒有被初始化的物件,此時b執行緒對uniquesingleton一通操作之後得出的的結果可能是有錯誤的(因為uniquesingleton 還沒完成初始化,讀出來的資料錯誤,寫回的資料也會錯誤)。

public

class

singleton

public singleton getinstance()

}}return uniquesingleton;

}}

為了解決上述問題,需要在uniquesingleton前加入關鍵字volatile。使用了volatile關鍵字後,重排序被禁止,所有的寫(write)操作都將發生在讀(read)操作之前。

至此,雙重檢查鎖就可以完美工作了。

volatile與單例模式

參考文獻 volatile關鍵字的作用 原理 保持記憶體可見性 所有執行緒都能看到共享資料的最新值。防止指令重排序。1 讀取前先從記憶體重新整理最新的值。2 寫入後立即同步回記憶體中。插入記憶體屏障來禁止重排序。例如 下面是基於保守策略的jmm記憶體屏障插入策略 在每個volatile寫操作的前面插...

(一)設計模式之單例模式 volatile

簡單理解 在記憶體中某個例項物件只有乙個,並由程式程序中的其他執行緒共享該例項。1 了解物件建立過程 第一步 分配記憶體空間 第二步 呼叫建構函式,初始化例項。2 禁止指令重排序 當建構函式在物件初始化的完成之前就完成了物件賦值,在記憶體中開闢一片儲存區域後直接返回記憶體的引用,但是這個時候還沒正真...

單例模式 雙層檢驗鎖 volatile

package com.utils.threads public class dl return instance public static void main string args 分析 加volatile的必要性 原因在於指令重排的存在,加入volatile可以禁止指令重排 當某乙個執行緒執...