多線程程式設計是 Java 開發中的進階課題,也是 Bug 溫床。由於執行順序的非確定性,傳統單步除錯往往會改變時序掩蓋問題。本文整理自 Sun 專家 Laura Bennett 的實戰經驗,系統闡述多線程除錯核心觀念、常見錯誤類型與最佳實務。
最有效的除錯工具必須能檢視每個執行緒的執行狀態。大多數 Java 錯誤源於線程間交互,而非單一線程邏輯錯誤。優秀的 IDE 除錯器應提供:
單步執行會人為減緩特定線程,破壞原本的並發時序,導致「海森堡 Bug」(觀測即消失的錯誤)無法重現。正確做法是使用記錄追蹤、鎖分析工具或專門的並發除錯器。
多線程同時存取同一記憶體位置且缺乏同步機制。表現為資料損毀、例外拋出或邏輯錯誤。預防:volatile、Atomic* 類別、synchronized 區塊。
經典案例:Thread1 持有 LockA 等待 LockB,Thread2 持有 LockB 等待 LockA。預防策略:
tryLock(timeout) 而非無限等待synchronized 方法鎖定關鍵區段多線程非原子性地讀寫共享變數。典型案例:i++ 非原子操作、long/double 64 位元變數在 32 位元 JVM 上非原子寫入。解決:AtomicLong、volatile、同步區塊。
不當同步導致效能退化或活鎖。垃圾收集期間所有線程暫停(Stop-the-world)可能暴露同步設計缺陷。注意:過度同步等同於單線程化,喪失並發優勢。
synchronized 實戰應用public void addElement(Patient newPatient) {
synchronized (lock) {
Log query = incomingPatient.getLog();
Result[] results = query.getAllSearchResults();
for (int k = 0; k < results.length; k++) {
// 更新共享表格資料
setMaxColSize(myTable);
tableContents.add(myTable);
}
}
}
關鍵點:鎖物件 lock 必須是共享且不可變的實例;同步區塊應盡可能精簡。
不同 JVM 實作線程優先級調度策略不同(如 Windows 時間片輪轉 vs Linux 完全公平調度)。必須在目標部署作業系統上實測,驗證同步邏輯正確性。
long/double 非原子寫入:使用 AtomicLong 或同步賦值。stop():已棄用,會導致鎖釋放不全、資料不一致。正確終止方式:run() 方法自然返回或檢查中斷旗標。isAlive() 會誤報,建立新線程實例才是正道。Thread.yield(),避免佔用排程器。Collections.unmodifiable* 降低同步需求。ExecutorService、ConcurrentHashMap、CountDownLatch 取代手動 wait/notify。jstack 或 kill -3 產生完整線程快照。重點檢查:
Object.wait() 或 Thread.join() 的線程使用 Java Mission Control、VisualVM 或商業工具(JProfiler、YourKit)監控:
多線程除錯的核心在於「觀察而不干擾」。掌握線程狀態分析、鎖競爭偵測、非侵入式追蹤三大技能,配合不可變設計與高層級並發框架,才能駕馭複雜並發系統。記住:最好的除錯是設計階段就預防錯誤。