高開銷物件初始化常需延遲至首次使用,但線程安全的延遲初始化暗藏陷阱。本文深度剖析雙重檢查鎖定(DCL)的失效根因,提供兩種經實證的正確方案。
public class UnsafeLazyInitialization {
private static Instance instance;
public static Instance getInstance() {
if (instance == null) // 1: 線程A執行
instance = new Instance(); // 2: 線程B執行
return instance;
}
}
線程A執行檢查時,線程B可能見到instance非null但物件尚未完成初始化。
public class SafeLazyInitialization {
private static Instance instance;
public synchronized static Instance getInstance() {
if (instance == null) instance = new Instance();
return instance;
}
}
synchronized導致性能開銷。若getInstance()高頻調用,吞吐量下降;低頻場景可接受。
public class DoubleCheckedLocking {
private static Instance instance;
public static Instance getInstance() {
if (instance == null) { // 4: 第一次檢查
synchronized (DoubleCheckedLocking.class) { // 5: 加鎖
if (instance == null) // 6: 第二次檢查
instance = new Instance(); // 7: 問題根源
}
}
return instance;
}
}
表面優勢:物件創建後無需獲取鎖直接返回。但第7行可能發生指令重排序,導致其他線程見到半初始化物件。
instance = new Instance()分解為三步偽代碼:
memory = allocate(); // 1: 分配記憶體 ctorInstance(memory); // 2: 初始化物件 instance = memory; // 3: 設置引用指向記憶體
JIT編譯器可能重排序為1→3→2:
memory = allocate(); instance = memory; // 引用已指向,但物件未初始化! ctorInstance(memory);
依據《Java語言規範》,單線程內重排序不得改變執行結果(intra-thread semantics)。上圖藍線顯示:只要2排在4前面,單線程語義滿足。

但多線程並發時(紅線虛箭頭標示錯誤讀取):

線程A重排序後,線程B在第4行判斷instance != null,隨後訪問未初始化物件。

volatile語義阻擋public class SafeDoubleCheckedLocking {
private volatile static Instance instance; // 關鍵:volatile
public static Instance getInstance() {
if (instance == null) {
synchronized (SafeDoubleCheckedLocking.class) {
if (instance == null)
instance = new Instance(); // volatile禁止重排序
}
}
return instance;
}
}
JDK 5新記憶體模型(JSR-133)增強volatile語義,禁止構造器寫入與引用賦值重排序。執行時序:

public class InstanceFactory {
private static class InstanceHolder {
public static Instance instance = new Instance();
}
public static Instance getInstance() {
return InstanceHolder.instance; // 觸發類初始化
}
}
JVM類初始化階段獲取唯一初始化鎖(LC),多線程並發初始化時自動同步:
condition等待happens-before關係)

類初始化鎖的happens-before保證:初始化線程的寫入操作,後續線程必定可見。
| 方案 | 優勢 | 適用場景 |
|---|---|---|
| volatile DCL | 支援實例欄位與靜態欄位延遲初始化 | 需對實例欄位線程安全延遲初始化 |
| 靜態內部類 | 代碼更簡潔,無需額外同步關鍵字 | 需對靜態欄位線程安全延遲初始化 |
延遲初始化降低啟動開銷但增加訪問成本。多數情況正常初始化優於延遲初始化。確需使用時:
volatile雙重檢查鎖定作者:zuoxiaolong(左潇龍)
出處:博客園左潇龍技術博客