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.該用靜態內部類來解決