Nested Monitor Lockout問題相似於死鎖,一個Nested Monitor Lockout的發生以下:html
線程1 持有A對象鎖進入同步塊
線程1 持有B對象鎖進入同步塊(當成功持有A對象鎖後)
線程1 調用B.wait()釋放B對象鎖,但不釋放A對象鎖
線程1 等待另外一個線程(調用B.notify())發送信號以繼續執行釋放鎖A
線程2 須要同時持有A對象鎖和B對象鎖才能給線程1發送信號
線程2 沒法獲取A對象鎖,由於A對象鎖已經被線程1持有
線程2 無限期的等待線程1釋放A對象鎖
線程1 無限期的等待線程2發送信號,以喚醒繼續執行;由於線程2只有在持有A對象鎖的狀況纔有辦法給線程1發送喚醒信號
複製代碼
上面的描述看起來有點抽象,接下來咱們來看一下一個簡陋SimpleLock的實現:java
public class SimpleLock {
private class Monitor {
}
private final Monitor monitor = new Monitor();
private boolean isLocked = false;
public void lock() throws InterruptedException {
synchronized (this) {
while (isLocked) {
synchronized (monitor) {
monitor.wait();
}
}
isLocked = true;
}
}
public void unLock() {
synchronized (this) {
isLocked = false;
synchronized (monitor) {
monitor.notify();
}
}
}
}
複製代碼
能夠注意到在lock()方法中的第一個synchronized
構造塊中傳入的是"this"。第二個synchronized
構造塊中傳入的是成員變量monitorObject
。當isLocked
爲false並不會有任何問題,線程不會調用到monitor.wait()
。但當isLocked
爲true時,則線程會調用到while()
循環內部的monitor.wait()
進入等待狀態。post
這裏的問題在於調用完monitor.wait()
方法後,線程只釋放了monitorObject
對象鎖,並無釋放「this」即當前SimpleLock實例對象鎖。SimpleLock實例對象鎖仍然被第一個線程持有。this
當線程須要給lock()方法中的monitor.wait()
發送信號時,它須要嘗試獲取this即當前SimpleLock實例對象鎖來進入synchronized(this)
同步代碼塊。此時它會無限期的等待着,由於SimpleLock實例對象鎖一直被第一個線程持有且永遠不會釋放。由於第一個線程釋放SimpleLock實例對象鎖須要另外一個線程給它發送喚醒信號來退出wait()
方法和退出synchronized(this)
同步代碼塊。spa
簡而言之就是第一個線程在lock()方法的同步代碼塊中等待着另外一個線程發送信號好讓它退出同步代碼塊。另外一個發送信號的線程則須要第一個線程退出lock()方法的同步代碼塊好讓它進入unLock()方法中的同步代碼塊來發送信號給第一個線程。線程
最終的結果是不管哪一個線程調用lock()和unLock()方法都會無限期的等待下去。這種問題咱們稱之爲Nested Monitor Lockout。設計
你也許會以爲你永遠不會像上文說起的例子那樣來實現一個Lock,即不會在一個使用對象鎖的同步代碼塊中調用wait()和notify(),但事實上這是真實發生的。當你設計的代碼相似於上文說起實例中所遇到的狀況時,問題就會發生了。如,在上一篇文章中實現一個FairLock的時候。當你但願每一個線程都去調用隊列中與之一一對應的對象的wait()方法,且在日後的時間裏去調用該對象的notify()以此來喚醒對應的線程的時候。code
一個公平鎖的簡單實現:htm
public class FairLock {
private class QueueObject {
}
private boolean isLocked = false;
private Thread lockingThread;
private List<QueueObject> waitingThreads = new ArrayList<>();
public void lock() throws InterruptedException {
// 爲每個線程建立與之一一對應的對象鎖
QueueObject queueObject = new QueueObject();
// 線程得到當前對象實例鎖進入同步代碼塊
synchronized (this) {
// 將當前線程對應的對象鎖添加到等待隊列中
waitingThreads.add(queueObject);
// 判斷當前FairLock是否爲鎖住對象,且判斷等待隊列中隊頭是否爲當前線程對應的對象鎖
while (isLocked || waitingThreads.get(0) != queueObject) {
// 得到當前線程對應的對象鎖進入同步代碼塊
synchronized (queueObject) {
try {
// 阻塞當前線程進入等待狀態
queueObject.wait();
} catch (InterruptedException e) {
// 若線程意外喚醒,則從等待隊列中移除
waitingThreads.remove(queueObject);
throw e;
}
}
}
// 當前FairLock,當前線程是第一個進入該同步代碼塊的線程
// 移除本線程在等待隊列中對應的對象鎖
waitingThreads.remove(queueObject);
// 取得FairLock鎖
isLocked = true;
lockingThread = Thread.currentThread();
}
}
public synchronized void unLock() {
// 檢查異常狀況,當前線程並不是持有FairLock線程
if (lockingThread != null && lockingThread != Thread.currentThread()) {
throw new IllegalMonitorStateException("Calling thread has not locked this lock");
}
// 釋放鎖
isLocked = false;
lockingThread = null;
// 等待隊列中有其餘線程在等待取得FairLock
if (waitingThreads.size() > 0) {
// 取得隊列頭部線程與之對應的對象鎖,
final QueueObject queueObject = waitingThreads.get(0);
// 取得該對象鎖
synchronized (queueObject) {
// 喚醒對象鎖對應的線程
queueObject.notify();
}
}
}
}
複製代碼
第一眼感受這個實現沒什麼問題,但咱們能夠注意處處在lock()方法兩個synchronized()
代碼塊中調用queueObject.wait()
的部分;實際上線程在執行完queueObject.wait()
後僅會釋放synchronized(queueObject)
所持有的queueObjct
對象鎖,並不會釋放synchronized(this)
所持有的"this"即FairLock實例對象鎖。對象
一樣須要注意的是unLock()方法簽名中聲明有synchronized
關鍵字,等同於synchronized(this)
代碼塊。這意味着若是一個線程在lock()方法中的synchronized(this)
代碼塊無限期的等待下去,那麼其餘線程將會被無限期的阻塞。調用unLock()方法的線程也會無限期的阻塞等待其餘線程釋放synchronized(this)
所持有的鎖以進入unLock()方法。一旦線程沒法進入unLock()方法就沒法給持有synchronized(this)
對象鎖的線程發送信號讓它退出等待狀態(退出wait()方法)從而退出synchronized(this)
代碼塊。
可見,上文FairLock的實現可以帶來Nested Monitor Lockout問題。一個改進的FairLock實現已經在飢餓與公平中說起。
從結果看Nested Monitor Lockout和死鎖的狀況十分相似:線程都會終結於互相等待彼此到永遠。
固然它們也不是那麼的類似。在線程死鎖與預防中咱們提到死鎖會在兩個線程以不一樣順序獲取相同的鎖的狀況下發生。線程1持有鎖A,嘗試獲取鎖B。線程2持有鎖B,嘗試獲取鎖A。一樣在線程死鎖與預防中,咱們提到可讓線程在獲取相同鎖的時候以相同順序的方式進行,以此來解決死鎖問題。但實際上,Nested Monitor Lockout問題就是按照順序來獲取相同的鎖。線程1持有鎖A和鎖B而且等待線程2發送信號。線程2須要獲取鎖A和鎖B來給線程1發送信號。因此狀況變成了一個線程在等待另外一個線程發送信號,另外一個線程則在等待它釋放鎖。
如下對兩種狀況進行描述:
在死鎖中,兩個線程互相等待對方釋放本身所須要的鎖。
在Nested Monitor Lockout中,線程1持有鎖A,等待線程2發送信號。線程2須要鎖A來給線程1發送信號。
複製代碼
該系列博文爲筆者複習基礎所著譯文或理解後的產物,複習原文來自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial