1. 程式人生 > >單例模式以及雙檢鎖DCL

單例模式以及雙檢鎖DCL

1、單例模式

      關於單例模式,就不再詳細敘述,想必大家都耳熟能詳了,簡單回顧下吧。以下是單例模式的一個例子:

  1. publicclass DoubleCheckedLock {  
  2.     privatestatic DoubleCheckedLock instance;    
  3.     publicstatic DoubleCheckedLock getInstance() {    
  4.         if (instance == null) {    
  5.             instance=new DoubleCheckedLock();  
  6.         }    
  7.         return instance;    
  8.     }    
  9. }  

      上述的例子,如果是在併發的情況下,就會遇到嚴重的問題。比如執行緒A在判斷instance為空時,進入new操作,new操作還未完成時,此時執行緒B也執行到判斷instance是否為NULL,那麼可能就會造成執行緒A和執行緒B都在new,那就違背了單例模式的原本含義了。那麼既然需要保證只有一個例項,我們是否可以通過synchronized關鍵字來解決呢?

  1. publicclass DoubleCheckedLock {  
  2.     privatestatic DoubleCheckedLock instance;    
  3.     publicstaticsynchronized DoubleCheckedLock getInstance() {    
  4.         if (instance == null) {    
  5.             instance=new DoubleCheckedLock();  
  6.         }    
  7.         return instance;    
  8.     }    
  9. }  

    不可否認,synchronized關鍵字是可以保證單例,但是程式的效能卻不容樂觀,原因在於getInstance()整個方法體都是同步的,這就限定了訪問速度。其實我們需要的僅僅是在首次初始化物件的時候需要同步,對於之後的獲取不需要同步鎖。因此,可以做進一步的改進:

  1. publicclass DoubleCheckedLock {  
  2.     privatestatic DoubleCheckedLock instance;    
  3.     publicstatic DoubleCheckedLock getInstance() {    
  4.         if (instance == null) {  //step1
  5.             synchronized (DoubleCheckedLock.class) { //step2
  6.                 if(instance==null){ //step3
  7.                     instance=new DoubleCheckedLock(); //step4
  8.                 }  
  9.             }  
  10.         }    
  11.         return instance;    
  12.     }    
  13. }  

    這樣我們將上鎖的粒度降低到了僅僅是初始化例項的那部分,從而使程式碼即正確又保證了執行效率。這就是所謂的“雙檢鎖”機制(顧名思義)。

     雙檢鎖機制的出現確實是解決了多執行緒並行中不會出現重複new物件,而且也實現了懶載入,但是很可惜,這樣的寫法在很多平臺和優化編譯器上是錯誤的,原因在於:instance=new DoubleCheckedLock()這行程式碼在不同編譯器上的行為是無法預知的。一個優化編譯器可以合法地如下實現 instance=new DoubleCheckedLock():

1. 給新的實體instance分配記憶體;

2. 呼叫DoubleCheckedLock的建構函式來初始化instance。

    現在想象一下有執行緒A和B在呼叫DoubleCheckedLock執行緒A先進入,在執行到步驟4的時候被踢出了cpu。然後執行緒B進入,B看到的是instance已經不是null了(記憶體已經分配),於是它開始放心地使用instance,但這個是錯誤的,因為A還沒有來得及完成instance的初始化,而執行緒B就返回了未被初始化的instance例項。

當我們結合Java虛擬機器的類載入過程就會更好理解。對於JVM載入類過程,我還不是很熟悉,所以簡要地介紹下:

jvm載入一個類大體分為三個步驟:
1)載入階段:就是在硬碟上尋找java檔案對應的class檔案,並將class檔案中的二進位制資料載入到記憶體中,將其放在執行期資料區的方法區中去,然後在堆區建立一個java.lang.Class物件,用來封裝在方法區內的資料結構
2)連線階段:這個階段分為三個步驟,步驟一:驗證,當然是驗證這個class檔案裡面的二進位制資料是否符合java規範;步驟二:準備,為該類的靜態變數分配記憶體空間,並將變數賦一個預設值,比如int的預設值為0;步驟三:解析,這個階段就不好解釋了,將符號引用轉化為直接引用,涉及到指標;
3)初始化階段:當我們主動呼叫該類的時候,將該類的變數賦於正確的值(這裡不要和第二階段的準備混淆了),舉個例子說明下兩個區別,比如一個類裡有private static int i = 5; 這個靜態變數在"準備"階段會被分配一個記憶體空間並且被
賦予一個預設值0,當道到初始化階段的時候會將這個變數賦予正確的值即5,瞭解了吧!

      因此,雙檢鎖對於基礎型別(比如int)適用。因為基礎型別沒有呼叫建構函式這一步。那麼對於雙檢鎖中因編譯器的優化無法保證執行順序的問題,具體地說是在C++下是精簡指令集(RISC)機器的編譯器會重新排列編譯器生成的組合語言指令,從而使程式碼能夠最佳運用RISC處理器的平行特性,因此有可能破壞雙檢鎖模式。對於此問題,查閱了不少解決方案,主要有以下幾種:

1)使用memory barrier,,關於merrory barrier的介紹,可參閱博文Memory barrier

2)java中可考慮volatile關鍵字定義新的語意來解決這個問題,關於volatile關鍵字的使用,可見博文volatile關鍵字