Java併發編程(Java Concurrency)(16)- 死鎖(Deadlock)

原文鏈接:http://tutorials.jenkov.com/java-concurrency/deadlock.html

摘要:這是翻譯自一個大概30個小節的關於Java併發編程的入門級教程,原作者Jakob Jenkov,譯者Zhenning Lang,轉載請註明出處,thanks and have a good time here~~~(希望自己不要留坑)

譯註:這一節中大量的提到了鎖的概念,但之前的教程中並未過多涉及。我們可以這樣理解這裏的“鎖”:假設有幾個代碼快都用 synchronized 進行了修飾,並且 synchronized 的參數也是相同的對象,那麼由之前的教程我們知道只有一個線程可以訪問這些代碼塊,感覺上像不像這個線程鎖住了這些代碼?亦或說這個線程鎖住了(得到了)那個作爲 synchronized 參數的對象,使得其他線程無法得到。

所以本文中經常提到某個線程持有一個鎖,實際上指的是這個線程進入了一個 synchronized 代碼段的意思。

1 線程死鎖

“死鎖”指的是兩個或更多的被阻塞的線程,需要來自於這些線程所持有的鎖來相互解除阻塞的狀態。死鎖可能發生在同一時刻多個線程都需要相同的一系列鎖來完成指定的任務,但所需要鎖的前後順序不同。

例如,如果線程 1 鎖住了 A 並且進而想鎖住 B,同時線程 2 鎖住了 B 並且進而想鎖住 A,此時就會發生死鎖。因爲線程 1 永遠也得不到 B (被線程 2 鎖住了),線程 2 也永遠也得不到 A (被線程 1 鎖住了)。此外,他們兩者也不知道發生了什麼,從而一直被鎖住。這種情況就是死鎖。

這種情況如下所示:

線程 1  鎖住 A, 等待 B
線程 2  鎖住 B, 等待 A

下面通過一個 TreeNode(樹結構節點) 類的示例來展示上面的死鎖是如何發生的:

public class TreeNode {

      TreeNode parent = null;  
      List children = new ArrayList();

      public synchronized void addChild(TreeNode child){
            if(!this.children.contains(child)) {
                  this.children.add(child);
                  child.setParentOnly(this);
            }
      }

      public synchronized void addChildOnly(TreeNode child){
            if(!this.children.contains(child){
                  this.children.add(child);
            }
      }

      public synchronized void setParent(TreeNode parent){
            this.parent = parent;
            parent.addChildOnly(this);
      }

      public synchronized void setParentOnly(TreeNode parent){
            this.parent = parent;
      }
}

如果線程 1 調用了 parent.addChild(child) 方法的同時,線程 2 調用了 child.setParent(parent) 方法,對於同一對父實例與子實例,死鎖的情況可能會發生,正如下面的僞代碼所示:

Thread 1: parent.addChild(child); //locks parent
          --> child.setParentOnly(parent);

Thread 2: child.setParent(parent); //locks child
          --> parent.addChildOnly()

下面對這個死鎖進行一個解釋。線程 1 會調用 parent.addChild(child) 方法,由於 addChild() 是同步的,所以實際上 parent 就會被鎖住(即其他線程無法進入 parent 的其他方法)。

類似的,線程 2 會調用 child.setParent(parent) 方法,由於 setParent() 是同步的,所以 child 就會被鎖住(即其他線程無法進入 parent 的其他方法)。

現在,child 和 parent 對象分別被兩個線程鎖住了。接下來,線程 1 嘗試調用 child.setParentOnly() 方法,但由於 child 對象被線程 2 鎖住了,所以線程 1 就被阻塞在了 addChild() 方法裏;類似的線程 2 也嘗試調用 parent.addChildOnly() 方法但 parent 對象被線程 1 鎖住了,導致線程 2 頁發生了阻塞。現在兩個線程都被阻塞了,並且都在等待對方釋放所持有的鎖。

注意:這兩個線程必須同時調用 parent.addChild(child) 與 child.setParent(parent) 方法,並且 parent 和 child 實例需要是正好相互調用的。所以上面的代碼可能會正常運行很長的時間,直到突然死鎖就發生了。

死鎖必須需要“同時”。例如,如果上面的例子中線程 1 比線程 2 提前一些執行,就會同時鎖住 parent(parent.addChild(child)方法) 和 child(child.setParentOnly()方法),那麼線程 2 在嘗試調用 child.setParent(parent) 時就會被先阻塞。這時死鎖就不會發生。由於線程調度通常是無法被預期的,所以沒有辦法預測何時會發生死鎖,只能說可能發生死鎖。

2 更加複雜的死鎖

死鎖的狀況也可能會發生在多於兩個線程身上 。這種情況更加難以被事先估計或實際觀測到(雖然可能程序運行很久很久都沒問題,但確實會在某個時刻發生死鎖)。下面的例子展示了四個線程的死鎖現象:

線程 1  鎖住 A, 等待 B
線程 2  鎖住 B, 等待 C
線程 3  鎖住 C, 等待 D
線程 4  鎖住 D, 等待 A

例子中線程 1 等待 2,2 等待 3,3 等待 4,4 又等待 1。

3 數據庫死鎖

關於死鎖更加複雜的情況發生在數據庫事物(database transaction,請查閱相關概念)處理中。數據庫事物處理可能包含一系列 SQL 更新(寫)請求。在一次數據庫事物處理中,當其中的一條記錄被更新後,那麼這條記錄就會被鎖住(即其他的數據庫事物無法處理這條數據),直到先前的數據庫事物被處理完畢。那麼相同的數據庫事物中的每個寫請求都可能會鎖住數據庫中的多條數據。

如果多個數據庫事物同時運行並需要更新一條記錄,就存在着死鎖的風險,例如:

數據庫事物 1:數據庫請求 1  ->  爲了寫記錄 1 而鎖住記錄 1 
數據庫事物 2:數據庫請求 1  ->  爲了寫記錄 2 而鎖住記錄 2 
數據庫事物 1:數據庫請求 2  ->  爲了寫記錄 2 而嘗試鎖住記錄 2 
數據庫事物 2:數據庫請求 2  ->  爲了寫記錄 1 而嘗試鎖住記錄 1

由於不同的請求中都存在着鎖,而且一個給定的數據庫事物中的所有鎖並非都是事先已知的,所以在數據庫事務處理中很難預測和阻止死鎖的發生。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章