這是併發程式設計系列的第八篇文章。上一篇介紹了任務的取消及關閉,這一篇說一下死鎖的問題。
哲學家問題
一提到死鎖,很多人都會想到哲學家問題。假設有5個哲學家,圍坐在一張圓桌旁,哲學家只做兩件事,吃飯和思考。吃飯的時候需要使用兩隻筷子,但是每個哲學家面前只放了一支筷子,如果想吃飯,必須借用旁邊哲學家的筷子。
這個時候如何管理每個哲學家吃飯與思考的時機和順序就變得很重要。如果每個哲學家,拿起自己面前的筷子的時候,發現旁邊的筷子不可用時,並不是釋放手裡筷子,而是死等旁邊的筷子,就會發生死鎖。所有哲學家都會因為等待對方的筷子而餓死。
死鎖抽象模型
多個執行緒相互持有彼此正在等待的鎖而又不釋放自己已經持有的鎖時就會發生死鎖現象。對於上面的哲學家問題,筷子就是鎖,而每位哲學家就是一個執行緒。
如果非要對死鎖的場景做個簡單的分類的話,那麼大概可以分為如下幾類。
鎖順序死鎖
如果一個執行緒需要持有兩把鎖才能幹活。比如執行緒A先拿到1號鎖,然後再拿2號鎖,而這個時候執行緒B搶先持有了2號鎖,而等待持有1號鎖。這個時候將造成死鎖,造成這種死鎖的原因是因為兩個執行緒獲取鎖的順序錯亂了,如果都是按照先拿1號鎖,再拿2號鎖的順序執行,就不會發生死鎖情況。
資源死鎖
假設一個任務需要對兩個資料庫進行連線,兩個資料庫的連線物件都從對應的資料庫連線池中獲取。 當執行緒A持有資料庫1的連線,等待資料庫2的連線,而執行緒B持有資料庫2的連線,而等待資料庫1的連線,如果任意一個數據庫連結池資源不足,那麼也將發生死鎖現象。
執行緒飢餓
還有一種情況也會發生執行緒阻塞,假設我們定義了只有一個執行緒的執行緒池,但是我們提交了2個任務,第一個任務依賴於第二任務,但是因為第二個任務進入了等待佇列(只有一個執行緒執行任務)。所以整個程式將會被阻塞住。
如何避免死鎖
透過上面對死鎖問題的產生原因進行分析,我們大概可以想到有那麼幾種方式可以避免死鎖。 第一種方式就是避免持有多個鎖,但是這種情況只適合比較簡單的業務場景。 第二種方式就是使用帶有超時時間的顯示鎖Lock,在規定的時間內獲取不到相應的鎖資源時則自動釋放已經持有的鎖資源,這樣就可以避免長期持有鎖而形成死鎖。
結束
死鎖問題很難被發現,即便你經歷了嚴格的測試。死鎖問題往往發生在線上高併發的場景下。所以各位在寫併發程式的時候,一定要仔細分析業務需求及自己實現的程式碼邏輯。