後端---深入理解雙重檢查鎖定
Java中的雙重檢查鎖定
雙重檢查鎖定又稱雙重效驗鎖,以前常常用於Java中的單例模式,在併發程式設計中的執行緒池中常常用到該模式,並且在Spring中DI(依賴注入)也用到該模式的思想,當Spring執行的時候將我們加入註解的bean(Java物件)遍歷出來,並建立其相關的一個例項,在程式的執行中,如果遇到要操作該物件的時候,便使用Spring為我們建立的該類的單例進行相關的操作。但是如何確保只生成一個單例呢?我在之前寫過一篇部落格,詳細的講述了單例模式的相關概念:
https://blog.csdn.net/weixin_42504145/article/details/85006406 後端---Java設計模式之單例模式詳解
有需要了解的朋友可以看看,在今天的這篇部落格中我們只是單獨講解一下雙重效驗鎖的一些知識和問題。
雙重檢查鎖定(DCL Double Checked Locking)的由來
在java程式中,有時候可能需要推延一些高開銷的物件進行初始化的操作,並且只有在使用這些物件的時候進行初始化。此時,程式設計師可能會採用延遲初始化。但要正確的實現執行緒安全的延遲初始化需要一些技巧,否則會出現一些問題,比如我們看下面這段程式碼:
SCL程式碼
public class UnsafeLazyInitialization{ private static Instance instance; public static Instance getInstance(){ if(instance==null) //1:A執行緒執行 instance=new Instance; //2:B執行緒執行 return instance; } }
我們可以看出在這段程式碼中,在UnsafeLazyInitialization中,假設A執行緒執行程式碼1的同時,Bxiancheng只想程式碼2.此時,執行緒A可能會看到instance引用的物件還沒有完成初始化(出現這種情況的原因是編譯器和處理器對我們的程式碼進行了重排序)
所以對這個類的getInstance()方法我們進行了一些優化,加鎖的程式碼如下:
public class UnsafeLazyInitialization{ private static Instance instance; public synchronized static Instance getInstance(){ if(instance==null) instance=new Instance; return instance; } }
我們對getInstance()的方法進行了加鎖處理,但是synchroized關鍵字導致了效能下降,如果這個方法被多個執行緒進行呼叫,將會導致程式的執行效能下降。所以就有了下面的DCL雙重效驗鎖定
public class DoubleCheckedLocking{ //1
private static Instance instance; //2
public static Instance getInstance(){ //3
if(instance==null) { //4 第一次檢查
synchronized{DoubleCheckedLocking.class}{ //5 加鎖
if(instance == null) //6 第二次檢查
instance = new Instance(); //7 問題的根源所在
} //8
} //9
return instance; //10
} //11
}
通過上面的程式碼我們可以看到,假如有多個方法進行呼叫,假如說第一次檢查不為null,即已經生成了一個單例,那麼就不要執行加鎖的程式碼,直接返回結果,可以大幅度的降低synchroized帶來的效能開銷,但是一個隱患出現了,當代碼執行到第4行的時候 程式碼讀取到的Instance不為null時,Instance引用的物件有可能還沒有完成初始化。
why???
這就要涉及到計算機底層的一些知識了,當我們編寫出一段程式碼的時候,程式碼會被計算機執行,生成我們想要的結果,程式碼之間有著順序的邏輯關係,但是在Cpu執行的時候會將我們的程式碼進行重排序,就是計算機並不一定保證程式碼的執行順序與你書寫程式碼的順序一致,在保證結果一致的情況下,Cpu會把程式碼執行的順序進行改變,從而達到效能的最大化。
我們瞭解到這份知識後,再來看剛給出的程式碼第7行(instance=new instance();建立了一個物件。這段程式碼又可以分解為下面3行虛擬碼:
memory=allocate(); // 1:分配物件的記憶體空間
ctroInstance(memory); // 2:初始化物件
instance=memory; // 3:設定Instance指向剛分配記憶體地址
所以當進行了重排序的時候,2和3的位置有可能互換,這樣會導致我們例項指標執行了一段空間不為null,但是並沒有真正完成初始化物件這個操作。
解決方案
1.給instance宣告成volatile (volatile關鍵字的語義會禁止一個共享變數的重排序)
2.該用靜態內部類來解決