現代企業級應用程式普遍具備高併發與多層架構特性,這兩大特性雖提升了系統吞吐量與擴展性,卻也引入了死鎖風險。併發導致資源爭用,而多層抽象則增加了問題隔離與修復的難度。本文將系統性探討 J2EE 環境下兩大類典型死鎖——資料庫死鎖與跨資源死鎖,並提供實戰導向的診斷與預防指南,內容同樣適用於其他技術棧。
資料庫死鎖發生於多個連線互相持有對方所需的鎖且均無法釋放時。其棘手之處在於鎖多為隱性獲取:更新資料列時隱性加鎖,於事務提交或回滾時釋放。鎖粒度(行/頁/表)取決於隔離級別、查詢計劃與資料庫引擎策略,且會隨資料量變化而動態調整。例如資料庫可能將 10 行鎖升級為頁鎖,導致無關資料行被誤阻塞。
更複雜的是,單一 SQL 可能觸發多索引維護、約束檢查、觸發器執行,每個動作都引入新鎖;其他應用同樣訪問同一模式,鎖順序難以預測。綜上,資料庫死鎖幾乎無法徹底消除,但幸運的是多數資料庫具備死鎖檢測機制:強制回滾代價最小的事務以釋放鎖,使其他連線得以繼續。
鑑於資料庫可自由獲取鎖,幾乎無法保證多線程不死鎖。工程上可行的方案是「遇到死鎖異常即重試整個事務」。具體實施要點:
在 J2EE 中,可透過 CMT(容器管理事務)配置 retry-on-deadlock 參數自動重試;若容器不支援,則需改用 BMT(Bean 管理事務)手動控制。值得注意的是,MVCC 資料庫(Oracle、PostgreSQL)因「讀不阻寫、寫不讀」特性,死鎖機率遠低於基於鎖的資料庫(SQL Server、MySQL MyISAM),選型時應充分考慮。
當死鎖跨越資料庫、JVM 鎖、連線池等多種資源時,單一環境無法檢測並恢復,後果往往是整個應用叢集逐漸凍結。此類死鎖具有傳染性:兩線程死鎖會阻塞所有嘗試訪問相關資源的線程,導致連鎖阻塞。診斷關鍵在於回答三個問題:
成因:外層 EJB 獲取連線 A,內層 RequiresNew EJB 需連線 B,池大小 N 滿足 N < 併發數 × (單線程最大連線數 - 1) + 1 時發生。
現象:線程轉儲顯示大量線程等待連線,連線池實為空且無閒置連線。
修復:擴大連線池、重構減少單線程連線數、或為內外層 EJB 配置獨立連線池。
典型代碼:
public void bulkLoadData(DataBatch batch) {
executeUpdate("update batch_status set status='Started' where batch_id=" + batchId); // 連線 1 持有寫鎖
updateBatchStatus(batchId, "Validated"); // RequiresNew,連線 2 等待同一行寫鎖 → 死鎖
...
}
本質:同一線程內,外層事務未提交前持有的鎖阻塞內層新連線;JVM 不允許外層調用返回前完成內層調用,資料庫無法感知此依賴關係,故不檢測死鎖。
修復:避免在同一事務中先更新後以 RequiresNew 再次更新同一資料;或將狀態更新延遲至事務提交後。
經典案例:synchronized 快取 get() 方法中執行資料庫查詢;另一線程更新資料庫並 clearCache() 需同一 JVM 鎖。基於鎖的資料庫中,更新鎖阻塞查詢,查詢線程持有 JVM 鎖阻塞更新線程 → 死鎖。
修復:持有 JVM 鎖時嚴禁執行資料庫/RPC 操作。重寫 get() 將查詢移出同步塊,並用 Thread.holdsLock(this) 斷言防護。
jstack/kill -3 獲取線程轉儲,及資料庫鎖監控視圖assert !Thread.holdsLock(lock)跨資源死鎖是 J2EE 系統穩定性的隱形殺手,但只要建立正確的架構約束與診斷流程,完全可控。願本文能為您的系統護航。