Java 的執行緒機制看似簡單,但併發程式設計暗藏殺機:在建構函式完成前讓 this 引用逸出,會導致不可預期的資料競爭與可見性問題。由於多執行緒錯誤難以重現、測試成本極高,從一開始就遵循「安全構造」規範至關重要。
當建構函式尚未完成,this 引用就被發布給其他執行緒可見的場所(靜態欄位、集合、事件監聽器、內部類實例、新啟動的執行緒),其他執行緒可能看到「半初始化」的物件,導致 NullPointerException、資料不一致或更隱蔽的邏輯錯誤。
public class EventListener {
public EventListener(EventSource source) {
// 初始化...
source.registerListener(this); // this 逸出!
}
public void onEvent(Event e) { ... }
}
若建立子類 RecordingEventListener,父類建構函式執行時子類欄位尚未初始化,事件到來會觸發 NullPointerException。
public class EventListener2 {
public EventListener2(EventSource source) {
source.registerListener(new EventListener() { // 隱含 this 參考
public void onEvent(Event e) { eventReceived(e); }
});
}
}
非靜態內部類實例持有外部類 this 引用,風險等同顯式傳遞 this。
新執行緒啟動前,建構函式可能尚未完成;若執行緒為內部類或接收 this 參數,同樣導致逸出。解決方案:提供 start() 方法,由呼叫端在建構完成後啟動執行緒。
只有讓其他執行緒可見的引用才屬於「發布」。
| 場景 | 安全性 | 理由 |
|---|---|---|
存入私有欄位 me = this | 安全 | 其他執行緒不可見 |
加入私有集合 set.add(this) | 安全 | 集合未發布 |
| 傳遞給內部類但未啟動執行緒 | 安全 | 引用未發布 |
存入靜態欄位 Unsafe.anInstance = this | 不安全 | 全域可見 |
| 傳遞給可能發布的方法 | 不安全 | 間接發布 |
在建構函式中 thread.start() | 不安全 | 新執行緒可見未完成物件 |
Thread.start() 前對執行緒 A 可見的變數,對執行緒 B 也可見。若在建構函式中啟動執行緒,這項保證失效。final 與 volatile 更強語義,但前提是建構期間 this 不逸出。start() 方法。「安全構造」不是可選項,而是編寫正確併發程式的前提。遵循簡單規範,即可在源頭消除一大類難以診斷的執行緒安全隱患。