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

嵌套管程鎖死

嵌套管程鎖死類似于死鎖, 下面是一個(gè)嵌套管程鎖死的場(chǎng)景:

線程 1 獲得 A 對(duì)象的鎖。
線程 1 獲得對(duì)象 B 的鎖(同時(shí)持有對(duì)象 A 的鎖)。
線程 1 決定等待另一個(gè)線程的信號(hào)再繼續(xù)。
線程 1 調(diào)用 B.wait(),從而釋放了 B 對(duì)象上的鎖,但仍然持有對(duì)象 A 的鎖。

線程 2 需要同時(shí)持有對(duì)象 A 和對(duì)象 B 的鎖,才能向線程 1 發(fā)信號(hào)。
線程 2 無(wú)法獲得對(duì)象 A 上的鎖,因?yàn)閷?duì)象 A 上的鎖當(dāng)前正被線程 1 持有。
線程 2 一直被阻塞,等待線程 1 釋放對(duì)象 A 上的鎖。

線程 1 一直阻塞,等待線程 2 的信號(hào),因此,不會(huì)釋放對(duì)象 A 上的鎖,
而線程 2 需要對(duì)象 A 上的鎖才能給線程 1 發(fā)信號(hào)……

你可以能會(huì)說(shuō),這是個(gè)空想的場(chǎng)景,好吧,讓我們來(lái)看看下面這個(gè)比較挫的 Lock 實(shí)現(xiàn):

//lock implementation with nested monitor lockout problem
public class Lock{
    protected MonitorObject monitorObject = new MonitorObject();
    protected boolean isLocked = false;

    public void lock() throws InterruptedException{
        synchronized(this){
            while(isLocked){
                synchronized(this.monitorObject){
                    this.monitorObject.wait();
                }
            }
            isLocked = true;
        }
    }

    public void unlock(){
        synchronized(this){
            this.isLocked = false;
            synchronized(this.monitorObject){
                this.monitorObject.notify();
            }
        }
    }
}

可以看到,lock()方法首先在”this”上同步,然后在 monitorObject 上同步。如果 isLocked 等于 false,因?yàn)榫€程不會(huì)繼續(xù)調(diào)用 monitorObject.wait(),那么一切都沒有問題 。但是如果 isLocked 等于 true,調(diào)用 lock()方法的線程會(huì)在 monitorObject.wait()上阻塞。

這里的問題在于,調(diào)用 monitorObject.wait()方法只釋放了 monitorObject 上的管程對(duì)象,而與”this“關(guān)聯(lián)的管程對(duì)象并沒有釋放。換句話說(shuō),這個(gè)剛被阻塞的線程仍然持有”this”上的鎖。

(校對(duì)注:如果一個(gè)線程持有這種 Lock 的時(shí)候另一個(gè)線程執(zhí)行了 lock 操作)當(dāng)一個(gè)已經(jīng)持有這種 Lock 的線程想調(diào)用 unlock(),就會(huì)在 unlock()方法進(jìn)入 synchronized(this)塊時(shí)阻塞。這會(huì)一直阻塞到在 lock()方法中等待的線程離開 synchronized(this)塊。但是,在 unlock 中 isLocked 變?yōu)?false,monitorObject.notify()被執(zhí)行之后,lock()中等待的線程才會(huì)離開 synchronized(this)塊。

簡(jiǎn)而言之,在 lock 方法中等待的線程需要其它線程成功調(diào)用 unlock 方法來(lái)退出 lock 方法,但是,在 lock()方法離開外層同步塊之前,沒有線程能成功執(zhí)行 unlock()。

結(jié)果就是,任何調(diào)用 lock 方法或 unlock 方法的線程都會(huì)一直阻塞。這就是嵌套管程鎖死。

一個(gè)更現(xiàn)實(shí)的例子

你可能會(huì)說(shuō),這么挫的實(shí)現(xiàn)方式我怎么可能會(huì)做呢?你或許不會(huì)在里層的管程對(duì)象上調(diào)用 wait 或 notify 方法,但完全有可能會(huì)在外層的 this 上調(diào)。 有很多類似上面例子的情況。例如,如果你準(zhǔn)備實(shí)現(xiàn)一個(gè)公平鎖。你可能希望每個(gè)線程在它們各自的 QueueObject 上調(diào)用 wait(),這樣就可以每次喚醒一個(gè)線程。

下面是一個(gè)比較挫的公平鎖實(shí)現(xiàn)方式:

//Fair Lock implementation with nested monitor lockout problem
public class FairLock {
    private boolean isLocked = false;
    private Thread lockingThread = null;
    private List waitingThreads =
        new ArrayList();

    public void lock() throws InterruptedException{
        QueueObject queueObject = new QueueObject();

        synchronized(this){
            waitingThreads.add(queueObject);

            while(isLocked ||
                waitingThreads.get(0) != queueObject){

                synchronized(queueObject){
                    try{
                        queueObject.wait();
                    }catch(InterruptedException e){
                        waitingThreads.remove(queueObject);
                        throw e;
                    }
                }
            }
            waitingThreads.remove(queueObject);
            isLocked = true;
            lockingThread = Thread.currentThread();
        }
    }

    public synchronized void unlock(){
        if(this.lockingThread != Thread.currentThread()){
            throw new IllegalMonitorStateException(
                "Calling thread has not locked this lock");
        }
        isLocked = false;
        lockingThread = null;
        if(waitingThreads.size() > 0){
            QueueObject queueObject = waitingThread.get(0);
            synchronized(queueObject){
                queueObject.notify();
            }
        }
    }
}
public class QueueObject {}

乍看之下,嗯,很好,但是請(qǐng)注意 lock 方法是怎么調(diào)用 queueObject.wait()的,在方法內(nèi)部有兩個(gè) synchronized 塊,一個(gè)鎖定 this,一個(gè)嵌在上一個(gè) synchronized 塊內(nèi)部,它鎖定的是局部變量 queueObject。

當(dāng)一個(gè)線程調(diào)用 queueObject.wait()方法的時(shí)候,它僅僅釋放的是在 queueObject 對(duì)象實(shí)例的鎖,并沒有釋放”this”上面的鎖。

現(xiàn)在我們還有一個(gè)地方需要特別注意, unlock 方法被聲明成了 synchronized,這就相當(dāng)于一個(gè) synchronized(this)塊。這就意味著,如果一個(gè)線程在 lock()中等待,該線程將持有與 this 關(guān)聯(lián)的管程對(duì)象。所有調(diào)用 unlock()的線程將會(huì)一直保持阻塞,等待著前面那個(gè)已經(jīng)獲得 this 鎖的線程釋放 this 鎖,但這永遠(yuǎn)也發(fā)生不了,因?yàn)橹挥心硞€(gè)線程成功地給 lock()中等待的線程發(fā)送了信號(hào),this 上的鎖才會(huì)釋放,但只有執(zhí)行 unlock()方法才會(huì)發(fā)送這個(gè)信號(hào)。

因此,上面的公平鎖的實(shí)現(xiàn)會(huì)導(dǎo)致嵌套管程鎖死。更好的公平鎖實(shí)現(xiàn)方式可以參考 Starvation and Fairness。

嵌套管程鎖死 VS 死鎖

嵌套管程鎖死與死鎖很像:都是線程最后被一直阻塞著互相等待。

但是兩者又不完全相同。在死鎖 中我們已經(jīng)對(duì)死鎖有了個(gè)大概的解釋,死鎖通常是因?yàn)閮蓚€(gè)線程獲取鎖的順序不一致造成的,線程 1 鎖住 A,等待獲取 B,線程 2 已經(jīng)獲取了 B,再等待獲取 A。如避免死鎖中所說(shuō)的,死鎖可以通過總是以相同的順序獲取鎖來(lái)避免。

但是發(fā)生嵌套管程鎖死時(shí)鎖獲取的順序是一致的。線程 1 獲得 A 和 B,然后釋放 B,等待線程 2 的信號(hào)。線程 2 需要同時(shí)獲得 A 和 B,才能向線程 1 發(fā)送信號(hào)。所以,一個(gè)線程在等待喚醒,另一個(gè)線程在等待想要的鎖被釋放。

不同點(diǎn)歸納如下:

死鎖中,二個(gè)線程都在等待對(duì)方釋放鎖。

嵌套管程鎖死中,線程 1 持有鎖 A,同時(shí)等待從線程 2 發(fā)來(lái)的信號(hào),線程 2 需要鎖 A 來(lái)發(fā)信號(hào)給線程 1。