死鎖是兩個或更多執行緒阻塞著等待其它處於死鎖狀態的執行緒所持有的鎖。死鎖通常發生在多個執行緒同時但以不同的順序請求同一組鎖的時候。
例如,如果執行緒1鎖住了A,然後嘗試對B進行加鎖,同時執行緒2已經鎖住了B,接著嘗試對A進行加鎖,這時死鎖就發生了。執行緒1永遠得不到B,執行緒2也永遠得不到A,並且它們永遠也不會知道發生了這樣的事情。為了得到彼此的物件(A和B),它們將永遠阻塞下去。這種情況就是一個死鎖。
該情況如下:
Thread 1 locks A, waits for B
Thread 2 locks B, waits for A
這裡有一個TreeNode類的例子,它呼叫了不同例項的synchronized方法:
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)方法,兩個執行緒中的parent表示的是同一個物件,child亦然,此時就會發生死鎖。下面的虛擬碼說明了這個過程:
Thread 1: parent.addChild(child); //locks parent
--> child.setParentOnly(parent);
Thread 2: child.setParent(parent); //locks child
--> parent.addChildOnly()
首先執行緒1呼叫parent.addChild(child)。因為addChild()是同步的,所以執行緒1會對parent物件加鎖以不讓其它執行緒訪問該物件。
然後執行緒2呼叫child.setParent(parent)。因為setParent()是同步的,所以執行緒2會對child物件加鎖以不讓其它執行緒訪問該物件。
現在child和parent物件被兩個不同的執行緒鎖住了。接下來執行緒1嘗試呼叫child.setParentOnly()方法,但是由於child物件現在被執行緒2鎖住的,所以該呼叫會被阻塞。執行緒2也嘗試呼叫parent.addChildOnly(),但是由於parent物件現在被執行緒1鎖住,導致執行緒2也阻塞在該方法處。現在兩個執行緒都被阻塞並等待著獲取另外一個執行緒所持有的鎖。
注意:像上文描述的,這兩個執行緒需要同時呼叫parent.addChild(child)和child.setParent(parent)方法,並且是同一個parent物件和同一個child物件,才有可能發生死鎖。上面的程式碼可能執行一段時間才會出現死鎖。
這些執行緒需要同時獲得鎖。舉個例子,如果執行緒1稍微領先執行緒2,然後成功地鎖住了A和B兩個物件,那麼執行緒2就會在嘗試對B加鎖的時候被阻塞,這樣死鎖就不會發生。因為執行緒排程通常是不可預測的,因此沒有一個辦法可以準確預測什麼時候死鎖會發生,僅僅是可能會發生。
更復雜的死鎖
死鎖可能不止包含2個執行緒,這讓檢測死鎖變得更加困難。下面是4個執行緒發生死鎖的例子:
Thread 1 locks A, waits for B
Thread 2 locks B, waits for C
Thread 3 locks C, waits for D
Thread 4 locks D, waits for A
執行緒1等待執行緒2,執行緒2等待執行緒3,執行緒3等待執行緒4,執行緒4等待執行緒1。
資料庫的死鎖
更加複雜的死鎖場景發生在資料庫事務中。一個資料庫事務可能由多條SQL更新請求組成。當在一個事務中更新一條記錄,這條記錄就會被鎖住避免其他事務的更新請求,直到第一個事務結束。同一個事務中每一個更新請求都可能會鎖住一些記錄。
當多個事務同時需要對一些相同的記錄做更新操作時,就很有可能發生死鎖,例如:
Transaction 1, request 1, locks record 1 for update
Transaction 2, request 1, locks record 2 for update
Transaction 1, request 2, tries to lock record 2 for update.
Transaction 2, request 2, tries to lock record 1 for update.
因為鎖發生在不同的請求中,並且對於一個事務來說不可能提前知道所有它需要的鎖,因此很難檢測和避免資料庫事務中的死鎖。