多線程下的單例模式DCL安全機制
寫在前面:歡迎來到「發奮的小張」的部落格。我是小張,一名普通的在校大學生。在學習之餘,用部落格來記錄我學習過程中的點點滴滴,也希望我的部落格能夠更給同樣熱愛學習熱愛技術的你們帶來收獲!希望大家多多關照,我們一起成長一起進步。也希望大家多多支援我鴨,喜歡我就給我一個關注吧!
最近在看陽哥的面試題,在多線程那裡深有感觸!
相信大家在學習單例模式的時候都寫過下面這樣的單例模式:
public class SingletonDemo {
private volatile static SingletonDemo instance=null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName()+"\t 我是構造方法SingletonDemo()");
}
/**
* @return 單例
*/
public static SingletonDemo getInstance(){//此處的方法可能同一時間被多個線程通路,是以會産生多個對象
if (instance==null){//線程1執行到此處。。。
instance = new SingletonDemo();//線程2執行到此處。。。
}
return instance;//線程3執行到此處。。。
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
SingletonDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
此時運作會發現,單例模式出現了多個對象!
這個單例模式在單線程模式下沒有任何問題,但是在多線程環境中就會出現多個instance對象。
在Java多線程中,我們有時候需要采用延遲初始化來降低初始化類和建立對象的開銷。DCL(雙端檢鎖機制)是常見的延遲初始化技術。
此處可能會有人會說,給方法加上鎖就可以解決了,但是加鎖會導緻性能大幅降低,是以是不劃算的!
是以,我們可以把上面的單例模式改造成如下形式:
public class SingletonDemo {
private static SingletonDemo instance=null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName()+"\t 我是構造方法SingletonDemo()");
}
/**
* DCL雙端檢鎖機制
* @return 單例
*/
public static SingletonDemo getInstance(){
if (instance==null){//第一次檢索,無法防止多線程
synchronized (SingletonDemo.class){//加鎖,保證安全性
if (instance==null){//第二次檢索
instance = new SingletonDemo();//此處有一個雷
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
SingletonDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
DCL 優勢:
上面的代碼的好處在這裡,如果第一次檢查 instance 不為 null ,那麼就不需要執行下面的加鎖和初始化操作。是以,可以大幅降低synchronized 帶來的性能開銷。
保證安全性:多個線程試圖在同一時間建立對象時,會通過加鎖來保證有一個線程建立對象。
保證性能:在對象建立好之後,執行 getInstance() 方法不需要擷取鎖,直接傳回已建立好的對象。
此時測試不會出現多個對象的情況!但是,我們也僅僅是測試了一部分資料,如果用海量的資料測試,會不會出現對個對象呢?
此處的雷就在這個 instance = new SingletonDemo() 這裡!
相信接觸過volatile的小夥伴都知道,它有三大作用:
- 可見性
- 不保證原子性
- 禁止指令重排
這裡,問題的關鍵就在于指令重排序的問題!
我們都知道,java虛拟機為了提高性能,在底層有一個指令重排序的過程!
是以,這裡的DCL并不完美,仍然不能保證多線程下的安全性!
在代碼進行第一次檢索時,代碼讀取到 instance 不為 null 時,instance 引用的對象有可能還沒有完成初始化。是以還是有可能産生安全問題!
解決DCL的指令重排序的安全問題
我們要解決這個問題,需要對instance對象加一個volatile來禁止它指令重排序!
代碼優化如下:
public class SingletonDemo {
private volatile static SingletonDemo instance=null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName()+"\t 我是構造方法SingletonDemo()");
}
/**
* DCL雙端檢鎖機制
* @return 單例
*/
public static SingletonDemo getInstance(){
if (instance==null){
synchronized (SingletonDemo.class){
if (instance==null){
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
SingletonDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
此時的單例模式在多線程下的DCL雙重檢測機制才算是線程安全的!
部落客後記:
此處的指令重排序部落客在這裡不詳細介紹了,因為涉及到jvm的底層知識點,以及彙編語言和作業系統的知識,部落客心知肚明道不清/(ㄒoㄒ)/~~!感興趣的小夥伴可以自行上網查詢相關資料!