嵌套管程鎖死類似于死鎖, 下面是一個(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ì)一直阻塞。這就是嵌套管程鎖死。
你可能會(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。
嵌套管程鎖死與死鎖很像:都是線程最后被一直阻塞著互相等待。
但是兩者又不完全相同。在死鎖 中我們已經(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。