在網上看到過好多篇文章在說明雙重檢查鎖在多個線程初始化一個單例類時到底爲什麼不行時在關鍵位置的描述模棱兩可,今天我們就來看一下爲什麼不能用雙重檢查鎖,問題到底出在了那裏?
下面我們直接進入主題,爲什麼使用雙重檢查鎖,原因是因爲在多線程初始化一個單例類時我們要確保得到一個對象,又想再確保一個對象時得到更高的效率,所以就有了雙重檢查鎖,使用雙重檢查鎖初始化對象的代碼如下
public class DoubleCheckedLocking { //1
private static DoubleCheckedLocking instance; //2
public static DoubleCheckedLocking getInstance() { //3
if (instance == null) { //4:第一次檢查
synchronized (DoubleCheckedLocking.class) { //5:加鎖
if (instance == null) //6:第二次檢查
instance = new DoubleCheckedLocking(); //7:問題的根源出在這裏
} //8
} //9
return instance; //10
} //11
}
爲什麼這樣是不行的,問題的根源出在第7行(instance = new DoubleCheckedLocking();),創建一個對象可以分解爲如下三步:
memory = allocate(); //1:分配對象的內存空間
ctorInstance(memory); //2:初始化對象
instance = memory; //3:設置instance指向剛分配的內存地址
上面三行僞代碼中的2和3之間,可能會被重排序,2和3之間重排序之後的執行時序如下:
memory = allocate(); //1:分配對象的內存空間
instance = memory; //3:設置instance指向剛分配的內存地址
//注意,此時對象還沒有被初始化!
ctorInstance(memory); //2:初始化對象
重排序不能影響單線程的執行語義,雖然這裏2和3進行了重排序,但是隻要保證2排在4前面執行,單線程內的執行結果不會被改變
時間 | 線程A | 線程B |
t1 | A1:分配對象的內存空間 | |
t2 | A3:設置instance指向內存空間 | |
t3 | B1:判斷instance是否爲空 | |
t4 | B2:由於instance不爲null,線程B將訪問instance引用的對象(而這個時候對象還沒有初始化) | |
t5 | A2:初始化對象 | |
t6 | A4:訪問instance引用的對象 |
總結,到此爲止我們只說明了爲什麼不可以用雙重檢查鎖來初始化對象
THE END!!!