Joshua Bloch 在《Effective Java》第 52 條中強烈建議:「文檔化線程安全性」。這是一條被反覆提及卻極少落實的良方。缺乏明確文檔時,使用者會對類的線程安全性做出錯誤假設——可能將非線程安全類當作安全類使用(後果嚴重),或試圖通過外部同步獲得安全性(可能不夠、甚至產生虛假安全感)。
當你編寫類時,對其併發行為的理解最深。事後補寫文檔不僅困難,而且容易遺漏關鍵細節。經典教訓:java.text.SimpleDateFormat 在 JDK 1.4 前未在 Javadoc 中說明非線程安全,導致無數開發者錯誤地創建靜態實例共享,在高負載下產生難以重現的錯誤。
Bloch 提出分類體系,核心在於「調用者是否需要/能夠使用外部同步」:
對象構建後狀態永不改變,天然線程安全,無需任何同步。如 String、Integer、BigInteger。
多線程環境下,無需外部同步,所有操作均符合規範。約束極嚴格,Hashtable、Vector 因方法間狀態依賴,實際屬於下一類。
單個操作線程安全,但特定操作序列需要外部同步。典型例子:遍歷 Vector 或 Hashtable 返回的 fail-fast 迭代器時,必須獨占鎖定底層集合。文檔必須明確指出哪些操作序列需要同步、使用哪把鎖。
類本身非線程安全,但可通過正確同步在併發環境中安全使用。如 ArrayList、HashMap、SimpleDateFormat、JDBC Connection/ResultSet。調用者可選擇同步每次調用、包裝同步代理、或同步操作序列。
無論是否外部同步,都無法在併發中安全使用。極少見,通常因修改靜態狀態影響其他線程。如調用 System.setOut() 的類。
即使所有方法同步,方法間的狀態依賴仍可能導致競態條件。例如遍歷 Vector:
for (int i = 0; i < v.size(); i++) {
doSomething(v.get(i)); // 另一線程刪除元素時會拋出異常
}
get() 的前置條件依賴 size() 的結果,必須在調用 size() 到 get() 期間獨占鎖定 Vector。
Hashtable 使用內部監視器,ConcurrentHashMap 則不允許外部鎖定。線程安全行為是類規範的固有部分,必須寫入文檔。雖無聲明式標記,但 Bloch 的五層體系提供優秀起點。若每個類都在 Javadoc 中說明線程安全層次,整個生態將受益匪淺。