來源:
Java并發編程的藝術
1 典型例子
雙重檢查鎖定的典型例子,如下:
public class DoubleCheckedLocking {
private static Instance instance;
/**
* 雙重檢查鎖定
*/
public static Instance getInstance() {
if (instance == null) { // 1、第一次檢查
synchronized (DoubleCheckedLocking.class) { // 2、加鎖
if (instance == null) { // 3、第二次檢查
instance = new Instance(); // 4、創建物件 (問題的根源在這里)
}
}
}
return instance;
}
}
這樣的雙重檢查鎖定是一個錯誤的優化!!!
當執行緒讀取到instance不為null的時候,instance參考的物件有可能還沒有完成初始化,
2 問題的根源
問題的根源在于 4、創建物件 的流程可能會被重排序,
- 創建物件的一般流程
創建物件的流程可以簡化為三步:分配記憶體空間、初始化物件、設定參考指向分配的記憶體地址,
memory = allocate(); // 1、分配物件的記憶體空間
initObject(memory); // 2、初始化物件
instance = memory; // 3、設定instance指向分配的記憶體地址
其中,2 和 3 之間可能被重排序,
- 重排序之后的流程
memory = allocate(); // 1、分配物件的記憶體空間
instance = memory; // 2、設定instance指向分配的記憶體地址
initObject(memory); // 3、初始化物件
那么,在發生重排序的情況下,A、B兩個執行緒執行這段雙重檢查鎖定的代碼時的執行順序可能是這樣的:
| 時間 | A 執行緒 | B 執行緒 |
|---|---|---|
| t1 | A1:分配物件的記憶體空間 | |
| t2 | A2:設定instance指向記憶體空間的地址 | |
| t3 | B1:判斷instance是否為null | |
| t4 | B2:由于instance不是null,B執行緒將直接訪問instance參考的物件 | |
| t5 | A3:初始化物件 | |
| t6 | A4:訪問instance參考的物件 |
按照以上的順序,B執行緒將會在instance物件還未初始化完成之前就訪問它,導致NPE,
3 解決方案
3.1 基于volatile的解決方案
對于雙重檢查鎖定來實作的延遲初始化方案,只需要將instance宣告為volatile型別,就能夠實作執行緒安全的延遲初始化:
public class SafeDoubleCheckedLocking {
private volatile static Instance instance; // 宣告為volatile型別
/**
* 雙重檢查鎖定
*/
public static Instance getInstance() {
if (instance == null) { // 1、第一次檢查
synchronized (DoubleCheckedLocking.class) { // 2、加鎖
if (instance == null) { // 3、第二次檢查
instance = new Instance(); // 4、創建物件
}
}
}
return instance;
}
}
volatile通過禁止重排序(初始化物件和設定instance指向分配的記憶體地址)來保證執行緒安全的延遲初始化,
3.2 基于類初始化的解決方案
Java語言規范規定,對于每一個類或者介面,都有唯一的一個初始化鎖與之對應,在多執行緒環境下,只有一個執行緒能夠獲取這個鎖并執行類的初始化,其他執行緒需要等待獲取這個鎖,這樣就能保證執行緒安全的類的初始化,
初始化一個類,包括執行這個類的靜態初始化和初始化在這個類中宣告的靜態欄位,
public class InstanceFactory {
private static class InstanceHolder {
public static Instance instance = new Instance();
}
public Instance getInstance() {
return InstanceHolder.instance; // 觸發InstanceHolder類的初始化
}
}
如上,當執行緒執行getInstance()方法時,將會觸發InstanceHolder類的初始化,此時只有一個執行緒能夠獲取InstanceHolder類的初始化鎖,并完成類的初始化程序,類中的靜態欄位instance將被初始化,Instance物件將被創建,雖然Instance物件創建程序中存在重排序,但是對于其他執行緒來說都是不可見的,
4 總結
- 大多數情況下,正常的初始化優于延遲初始化,
- 基于
volatile的延遲初始化方案有一個優勢,就是除了可以對靜態欄位實作延遲初始化之外,還可以對實體欄位實作延遲初始化, - 如果確實需要對實體欄位實作執行緒安全的延遲初始化,請使用基于
volatile的延遲初始化方案, - 如果確實需要對靜態欄位實作執行緒安全的延遲初始化,請優先使用基于類初始化的方案,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/141986.html
標籤:其他
上一篇:破解彩虹貓病毒
