問題起源
事情得從一個多線程編程里面臭名昭著的問題"Lost wake-up problem"說起。
這個問題並不是說只在Java語言中會出現,而是會在所有的多線程環境下出現。
假如我們有兩個線程,一個消費者線程,一個生產者線程。生產者線程的任務可以簡化成將count加一,而后喚醒消費者;消費者則是將count減一,而后在減到0的時候陷入睡眠,代碼如下:
生產者偽代碼:
count+1;
notify();
消費者偽代碼:
while(count<=0) wait() count--
熟悉多線程的朋友一眼就能夠看出來,這里面有問題。什么問題呢?
生產者是兩個步驟:
- count+1;
- notify();
消費者也是兩個步驟:
- 檢查count值;
- 睡眠或者減一;

這就是所謂的lost wake up問題。(丟掉了喚醒線程的那條信息)
那么怎么解決這個問題呢?
現在我們應該就能夠看到,問題的根源在於,消費者在檢查count到調用wait()之間,count就可能被改掉了。這就是一種很常見的競態條件。很自然的想法是,讓消費者和生產者競爭一把鎖,競爭到了的,才能夠修改count的值。
於是生產者的代碼是:
tryLock() count+1 notify() releaseLock()
消費者的代碼是:
tryLock() while(count <= 0) wait() count-1 releaseLock
注意的是,我這里將兩者的兩個操作都放進去了同步塊中。現在來思考一個問題,生產者代碼這樣修改行不行?
答案是,這樣改毫無卵用,依舊會出現lost wake up問題,而且和無鎖的表現是一樣的。
終極答案
所以,我們可以總結到,為了避免出現這種lost wake up問題,在這種模型之下,總應該將我們的代碼放進去的同步塊中。
Java強制我們的wait()/notify()調用必須要在一個同步塊中(不然的話會報錯),就是不想讓我們在不經意間出現這種lost wake up問題。
不僅僅是這兩個方法,包括java.util.concurrent.locks.Condition的await()/signal()也必須要在同步塊中:
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
@Test
public void test() { try { condition.signal(); } catch (Exception e) { e.printStackTrace(); } }
確的來說,即便是我們自己在實現自己的鎖機制的時候,也應該要確保類似於wait()和notify()這種調用,要在同步塊內,防止使用者出現lost wake up問題。
Java的這種檢測是很嚴格的。它要求的是,一定要處於相同鎖對象的同步塊中。舉例當不同鎖對象的時候來說:
private Object obj = new Object(); private Object anotherObj = new Object(); @Test public void produce() { synchronized (obj) { try { anotherObj.notify(); } catch (Exception e) { e.printStackTrace(); } }
}
這樣是沒有什么卵用的。一樣出現IllegalMonitorStateException。
所以大家知道該怎么做了吧?哈哈