鍍金池/ 教程/ Java/ 死鎖
Slipped Conditions
阻塞隊(duì)列
無阻塞算法
嵌套管程鎖死
Java 并發(fā)性和多線程介紹
死鎖
線程安全及不可變性
并發(fā)編程模型
Java 中的讀/寫鎖
剖析同步器
競態(tài)條件與臨界區(qū)
多線程的優(yōu)點(diǎn)
CAS
線程通信
如何創(chuàng)建并運(yùn)行 java 線程
阿姆達(dá)爾定律
避免死鎖
信號(hào)量
多線程的代價(jià)
饑餓和公平
線程池
重入鎖死
Java 中的鎖
Java 內(nèi)存模型
線程安全與共享資源
Java 同步塊

死鎖

死鎖是兩個(gè)或更多線程阻塞著等待其它處于死鎖狀態(tài)的線程所持有的鎖。死鎖通常發(fā)生在多個(gè)線程同時(shí)但以不同的順序請(qǐng)求同一組鎖的時(shí)候。

例如,如果線程 1 鎖住了 A,然后嘗試對(duì) B 進(jìn)行加鎖,同時(shí)線程 2 已經(jīng)鎖住了 B,接著嘗試對(duì) A 進(jìn)行加鎖,這時(shí)死鎖就發(fā)生了。線程 1 永遠(yuǎn)得不到 B,線程 2 也永遠(yuǎn)得不到 A,并且它們永遠(yuǎn)也不會(huì)知道發(fā)生了這樣的事情。為了得到彼此的對(duì)象(A 和 B),它們將永遠(yuǎn)阻塞下去。這種情況就是一個(gè)死鎖。

該情況如下:

Thread 1  locks A, waits for B
Thread 2  locks B, waits for A

這里有一個(gè) TreeNode 類的例子,它調(diào)用了不同實(shí)例的 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 調(diào)用 parent.addChild(child)方法的同時(shí)有另外一個(gè)線程 2 調(diào)用 child.setParent(parent)方法,兩個(gè)線程中的 parent 表示的是同一個(gè)對(duì)象,child 亦然,此時(shí)就會(huì)發(fā)生死鎖。下面的偽代碼說明了這個(gè)過程:

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

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

首先線程 1 調(diào)用 parent.addChild(child)。因?yàn)?addChild()是同步的,所以線程 1 會(huì)對(duì) parent 對(duì)象加鎖以不讓其它線程訪問該對(duì)象。

然后線程 2 調(diào)用 child.setParent(parent)。因?yàn)?setParent()是同步的,所以線程 2 會(huì)對(duì) child 對(duì)象加鎖以不讓其它線程訪問該對(duì)象。

現(xiàn)在 child 和 parent 對(duì)象被兩個(gè)不同的線程鎖住了。接下來線程 1 嘗試調(diào)用 child.setParentOnly()方法,但是由于 child 對(duì)象現(xiàn)在被線程 2 鎖住的,所以該調(diào)用會(huì)被阻塞。線程 2 也嘗試調(diào)用 parent.addChildOnly(),但是由于 parent 對(duì)象現(xiàn)在被線程 1 鎖住,導(dǎo)致線程 2 也阻塞在該方法處?,F(xiàn)在兩個(gè)線程都被阻塞并等待著獲取另外一個(gè)線程所持有的鎖。

注意:像上文描述的,這兩個(gè)線程需要同時(shí)調(diào)用 parent.addChild(child)和 child.setParent(parent)方法,并且是同一個(gè) parent 對(duì)象和同一個(gè) child 對(duì)象,才有可能發(fā)生死鎖。上面的代碼可能運(yùn)行一段時(shí)間才會(huì)出現(xiàn)死鎖。

這些線程需要同時(shí)獲得鎖。舉個(gè)例子,如果線程 1 稍微領(lǐng)先線程 2,然后成功地鎖住了 A 和 B 兩個(gè)對(duì)象,那么線程 2 就會(huì)在嘗試對(duì) B 加鎖的時(shí)候被阻塞,這樣死鎖就不會(huì)發(fā)生。因?yàn)榫€程調(diào)度通常是不可預(yù)測(cè)的,因此沒有一個(gè)辦法可以準(zhǔn)確預(yù)測(cè)什么時(shí)候死鎖會(huì)發(fā)生,僅僅是 可能會(huì)發(fā)生。

更復(fù)雜的死鎖

死鎖可能不止包含 2 個(gè)線程,這讓檢測(cè)死鎖變得更加困難。下面是 4 個(gè)線程發(fā)生死鎖的例子:

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。

數(shù)據(jù)庫的死鎖

更加復(fù)雜的死鎖場景發(fā)生在數(shù)據(jù)庫事務(wù)中。一個(gè)數(shù)據(jù)庫事務(wù)可能由多條 SQL 更新請(qǐng)求組成。當(dāng)在一個(gè)事務(wù)中更新一條記錄,這條記錄就會(huì)被鎖住避免其他事務(wù)的更新請(qǐng)求,直到第一個(gè)事務(wù)結(jié)束。同一個(gè)事務(wù)中每一個(gè)更新請(qǐng)求都可能會(huì)鎖住一些記錄。

當(dāng)多個(gè)事務(wù)同時(shí)需要對(duì)一些相同的記錄做更新操作時(shí),就很有可能發(fā)生死鎖,例如:

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.

因?yàn)殒i發(fā)生在不同的請(qǐng)求中,并且對(duì)于一個(gè)事務(wù)來說不可能提前知道所有它需要的鎖,因此很難檢測(cè)和避免數(shù)據(jù)庫事務(wù)中的死鎖。