通常,當同時執行兩個或兩個以上的線程時,如果每個線程都占有一個資源并請求另一個資源,這時就會出現死鎖情況。因為如果一個線程不能獲取資源,則所有線程都不能繼續執行,我們稱那個特定的線程被阻塞;如果每個線程都由于同組中另一個線程所占有的資源而被阻塞,我們就稱這個線程組被死鎖。
在本文中,我們將討論發生在典型的重要J2EE應用程序中的兩大類死鎖情況:“簡單”數據庫死鎖和跨資源死鎖。雖然我們的討論基于J2EE平臺,但也適用于其他技術平臺。
數據庫死鎖
在數據庫中,如果一個連接占用了另一個連接所需的數據庫鎖,則它可以阻塞另一個連接。如果兩個或兩個以上的連接相互阻塞,則它們都不能繼續執行,這種情況稱為死鎖。
數據庫死鎖問題不易處理,這是因為涉及到的鎖定通常不是顯式的。通常,對數據行進行隱式更新時,需要鎖定該數據行,執行更新,然后在提交或回滾封閉事務時釋放鎖。由于數據庫平臺、配置的隔離級以及查詢提示的不同,獲取的鎖可能是細粒度或粗粒度的,它會阻塞(或不阻塞)其他對同一數據行、表或數據庫的查詢。
獲取的鎖依賴于內部生成的查詢計劃。當數據大小和分步隨時間發生變化時,該計劃也可能改變。這樣在一個環境中獲取一組鎖的查詢可以嘗試在另一個環境中獲取一組完全不同的鎖。必要時,數據庫可以隨意地增加它的鎖。例如,數據庫可能會選擇鎖定整頁,而不是鎖定同一數據頁中的10個數據行,這會阻塞對無需鎖定的數據行的讀寫權限。
基于數據庫模式,讀寫操作會要求遍歷或更新多個索引、驗證約束、執行觸發器等。每個要求都會引入更多鎖。此外,其他應用程序還可能正在訪問同一數據庫模式中的某些對象,并獲取不同于您的應用程序所具有的鎖。
所有這些因素綜合在一起,數據庫死鎖幾乎不可能被消除了。值得慶幸的是,數據庫死鎖通常是可恢復的:當數據庫發現死鎖時,它會強制銷毀一個連接(通常是使用最少的連接),并回滾其事務。這將釋放所有與已經結束的事務相關聯的鎖,至少允許其他連接中有一個可以獲取它們正在被阻塞的鎖。
由于數據庫具有這種典型的死鎖處理行為,所以當出現數據庫死鎖問題時,數據庫常常只能重試整個事務。當數據庫連接被銷毀時,會拋出可被應用程序捕獲的異常,并標識為數據庫死鎖情況。如果允許死鎖異常傳播到初始化該事務的代碼層之外,則該代碼層可以只啟動一個新事務并重做先前所有工作。要正確使用此策略,則在事務成功提交之前,它的代碼不能有其他操作。注意:要限制重試次數,否則易導致死鎖的代碼塊會永久循環下去。
如果出現問題就重試,這種方法有點笨。但是,由于數據庫可以自由地獲取鎖,所以幾乎不可能保證兩個或兩個以上的線程不發生數據庫死鎖。此方法至少能保證在出現某些罕見的數據庫死鎖情況時,應用程序能正常運行。這比要求用戶去重試操作要好得多。
在J2EE應用程序中,開發人員可以設置一個EJB調用以使用Bean托管事務(BMT)——開發人員啟動、提交或回滾特定的事務或容器托管事務(CMT)——調用方法前啟動事務,并在方法完成后提交或回滾事務。如果EJB供應商提供retry-on-deadlock參數,從而可以通過容器托管事務自動完成此操作,那當然再好不過了。如果沒有這種自動功能,開發人員最終將僅為了對死鎖進行重試而強制EJB調用使用Bean托管事務。
遇到死鎖問題和鎖定其他線程的鎖的具體頻率在很大程度上取決于數據庫平臺、硬件、數據庫模式和查詢。在使用基于鎖的并發控制的數據庫(如MSSQL)中,未提交的寫操作會阻止讀操作,而未提交的讀操作會阻止寫操作,使數據庫更易出現死鎖問題。在多版本并發控制(MVCC)數據庫(如Oracle)中,未提交的寫操作不阻止讀操作——讀操作僅查看舊版本數據行。這雖然會引入其他問題,但不會造成同樣多的死鎖機會。我們要讓自己熟悉這些數據庫鎖定模式,并注意自己正在使用的類型。
在查找、修復以及避免數據庫死鎖方面,有一些很好的參考方法,但它們都不能徹底消除死鎖的可能性。
跨資源死鎖
當死鎖情況不完全局限于數據庫時,將更難找到它。數據庫對占有和請求的鎖有識別能力,所以能檢測整個數據庫中的死鎖;此外,數據庫事務在確定哪些東西是原子、哪些不是方面提供了一個良好的界線,所以能輕松地回滾事務,使其從死鎖中恢復。其他環境(如Java虛擬機)中的死鎖或可跨環境的死鎖更加危險,因為環境不能(或沒有)檢測到這些死鎖并嘗試恢復。更糟糕的是,這些死鎖會產生綜合效果——如果兩個線程占有某些資源集時出現死鎖,則其他任何嘗試訪問其中一個資源的線程也將被阻塞,該線程已經獲取的所有資源也被阻塞。這些死鎖常常不易發現,但對常見模式有一定的了解將有助于識別和修復死鎖問題。
當環境中出現可疑的死鎖情況時,您就需要考慮一些問題了。這些問題的答案將說明您正在處理的情形是下列情形中的哪一種(如果有的話),并提供了修復以下問題的詳細信息。要考慮的一些重要事項包括:
- 涉及什么線程,它們的調用堆棧是什么?這需要進行一些詳細的分析,將實際的死鎖線程從那些只是被死鎖的線程阻塞了的線程中分離出來。
- 這種死鎖情況總是在特定的代碼路徑中出現(每次執行這些特定的操作時),還是依賴于兩個或兩個以上同時執行的代碼路徑呢?
- 涉及的數據庫連接是什么?每個連接占有的數據庫鎖是什么?每個連接嘗試獲取的數據庫鎖是什么?每個數據庫連接響應的Java虛擬機線程是什么?