本文首發(fā)于http://www.itdecent.cn/p/18769b7dc46f
道生一,一生二,二生三,三生萬(wàn)物。
老子《道德經(jīng)》
Java是單繼承模型,有類似于老子的道德經(jīng)的哲學(xué),所有的類最終都會(huì)繼承自一個(gè)原始類,這個(gè)類就是Object。
Object對(duì)象中總共有11個(gè)可供protect或public方法。除去toString方法用于生成類的可讀化表示外,這些方法可以按其用途分為以下四類:
- 類的表示及反射
getClass - 類與Map的聯(lián)動(dòng)
equals,hashCode - Java的同步抽象
wait(三個(gè)重載),notify,notifyAll - Java中對(duì)象內(nèi)存表示及垃圾收集
finalize,clone
本文結(jié)合Java語(yǔ)言規(guī)范 (Java 8) 介紹wait和notify方法的規(guī)范及其在同步中的應(yīng)用。
方法定義及規(guī)范
Object類中關(guān)于wait和notify方法的源碼如下:
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
public final void wait() throws InterruptedException {
wait(0);
}
其中wait方法的兩個(gè)不同的重載都是wait(timeout)方法的輔助方法,值得一提的是方法wait(long timeout, int nanos)中主動(dòng)放棄了納秒級(jí)精度。由于常用的計(jì)算機(jī)系統(tǒng)都不是硬實(shí)時(shí)系統(tǒng),因此參數(shù)中的timeout都只能是一個(gè)粗略值。
Java語(yǔ)言規(guī)范中關(guān)于wait和notify的部分位于第17章,這一章主要介紹Java的內(nèi)存模型,同步語(yǔ)義及抽象。
規(guī)范中關(guān)于wait方法的定義如下
假定線程t在對(duì)象m上執(zhí)行方法wait, n為t執(zhí)行的lock次數(shù)。
- 如果
n == 0,即t還未獲得對(duì)象m的鎖,那么會(huì)拋出IllegalMonitorStateException, 這也就是為什么wait與notify方法需要在sychronzied段中執(zhí)行。 - 如果
t被中斷,那么會(huì)拋出InterruptedException且t的中斷狀態(tài)會(huì)被設(shè)置成false
否則
- 線程
t會(huì)被加入對(duì)象m的等待集合,并且在對(duì)象m上執(zhí)行n次unlock操作。 -
t將不會(huì)執(zhí)行任何后續(xù)的指令,直到它從m的等待集合中移除。
線程會(huì)因?yàn)橐韵挛宸N原因從等待集合中移除,并在之后的某個(gè)時(shí)間恢復(fù)運(yùn)行。
m.notifym.notifyAllt.interrupt-
timeout超時(shí) -
spurious wake-ups。JVM允許此方法實(shí)現(xiàn)為隨機(jī)喚醒,因此處于wait狀態(tài)的線程可能沒(méi)有任何原因就被喚醒,這也是為什么使用wait方法時(shí)需要將其放在循環(huán)體中的原因。
-
t在對(duì)象m上執(zhí)行n次lock操作。 - 如果
t是由于調(diào)用t.interrupt從等待集合中移除,那么t的中斷狀態(tài)會(huì)置為false
規(guī)范中關(guān)于notify和notifyAll方法的定義如下:
線程t,對(duì)象m,n為t在m上執(zhí)行的lock次數(shù)。
- 如果
n == 0, 則拋出IllegalMonitorStateException - 如果
n > 0, 執(zhí)行notify會(huì)將m的等待集合中某個(gè)線程移除。 - 如果
n > 0, 執(zhí)行nofityAll會(huì)將m的等待集合中的所有線程移除。
規(guī)范中關(guān)于中斷的定義如下:
線程中斷有以下兩種方法來(lái)執(zhí)行
Thread.interrupt
ThraedGroup.interrupt
假設(shè)線程t執(zhí)行線程u的中斷方法u.interrupt(t和u可能是同一個(gè)線程),這個(gè)調(diào)用會(huì)使得t的interruption狀態(tài)變成true.
如果u處于某個(gè)對(duì)象m的等待集合中,這將會(huì)使得u從wait方法中恢復(fù),并且拋出InterruptedExcetion
可以通過(guò)Thread.isInterrupted方法來(lái)判斷一個(gè)方法是否處于中斷狀態(tài)。Thread.interrupted靜態(tài)方法可用于一個(gè)線程獲得它的中斷狀態(tài)并清空中斷狀態(tài)。
JVM字節(jié)碼
分別查看以下wait方法和notify方法對(duì)應(yīng)的字節(jié)碼,結(jié)果如下:
public static void main(String[] args) throws Exception {
Object obj = new Object();
synchronized (obj) {
obj.wait();
}
}
-----
....
11: monitorenter
12: aload_1
13: invokevirtual #3 // Method java/lang/Object.wait:()V
16: aload_2
17: monitorexit
...
public static void main(String[] args) throws Exception {
Object obj = new Object();
synchronized (obj) {
obj.notify();
}
}
-----
...
11: monitorenter
12: aload_1
13: invokevirtual #3 // Method java/lang/Object.notify:()V
16: aload_2
17: monitorexit
...
從字節(jié)碼中可以看出,wait和notify功能由JVM的實(shí)現(xiàn),對(duì)應(yīng)的字節(jié)碼只是獲得相應(yīng)的監(jiān)視器鎖,并執(zhí)行相應(yīng)的native方法。
代碼實(shí)例分析
-
wait與notify方法一定需要放在sychronized中
分析以下代碼段
public static void main(String[] args) throws Exception{
Object obj = new Object();
obj.wait(); // throw java.lang.IllegalMonitorStateException
obj.notify(); // throw java.lang.IllegalMonitorStateException
}
根據(jù)JVM規(guī)范,若線程沒(méi)有獲得obj的監(jiān)視器鎖,即規(guī)范中n == 0的情況下,會(huì)拋出IllegalMonitorStateException。
為什么需要在執(zhí)行wait和notify方法時(shí)先獲得對(duì)象鎖呢?從規(guī)范中可以看出,wait和notify操作需要對(duì)對(duì)象的等待集合進(jìn)行更改,而這兩個(gè)更改本身就是競(jìng)態(tài)條件,因此需要同步。
在JVM的wait方法的實(shí)現(xiàn)中,需要釋放已經(jīng)獲得對(duì)象監(jiān)視器鎖,從而允許執(zhí)行notify的代碼段獲得鎖并執(zhí)行。
- 事件有序
interrupt與notify事件必然以一定順序發(fā)生,一個(gè)在wait中的線程,同時(shí)被另外兩個(gè)線程notify和interupt時(shí),線程要么被中斷,且通知被另外一個(gè)等待的線程獲取;要么線程先聽(tīng)到通知恢復(fù)執(zhí)行,未拋出InterruptException,線程的interuptted狀態(tài)變成true。
可以由以下代碼驗(yàn)證:
public static void test() throws Exception {
Object obj = new Object();
CountDownLatch latch = new CountDownLatch(1);
Thread t1 = new Thread(() -> {
synchronized (obj) {
try {
obj.wait();
System.out.println(Thread.currentThread().getName()
+ " be notify first");
latch.countDown();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()
+ " be interrupt first");
}
}
});
t1.start();
new Thread(() -> {
synchronized (obj) {
try {
obj.wait();
if (latch.getCount() > 0) {
System.out.println(Thread.currentThread().getName()
+ " count down latch");
latch.countDown();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
try {
TimeUnit.MICROSECONDS.sleep(100);
} catch (Exception ignore) {
}
t1.interrupt();
}).start();
new Thread(() -> {
synchronized (obj) {
try {
TimeUnit.MICROSECONDS.sleep(100);
} catch (Exception ignore) {
}
obj.notify();
}
}).start();
try {
latch.await(5, TimeUnit.SECONDS);
System.out.println("latch can exit");
} catch (Exception e) {
// will never got here
System.err.println("latch can not exit");
}
synchronized (obj) {
obj.notifyAll();
}
}
public static void main(String[] args) throws Exception {
for (int i = 0; i < 1000; i++) {
test();
}
}
使用wait和notify
- 生產(chǎn)者和消費(fèi)者問(wèn)題
并發(fā)問(wèn)題的一個(gè)經(jīng)典場(chǎng)景是生產(chǎn)者和消費(fèi)者的問(wèn)題,有多個(gè)線程生產(chǎn),另外多個(gè)線程消費(fèi)。應(yīng)對(duì)這個(gè)問(wèn)題通常會(huì)使用阻塞隊(duì)列。
假定隊(duì)列的接口是put和take,put用于生產(chǎn),take用于消費(fèi)。非線程安全的queue用于存放元素。使用對(duì)象m用的wait和notify方法來(lái)實(shí)現(xiàn)同步。
put的偽代碼如下
put (ele) {
sychronized(m) {
while (queue.size() == capacity) {
m.wait();
}
queue.add(ele);
m.notifyAll();
}
}
take的偽代碼如下
take () {
sychronized(m) {
while (queue.size() == 0) {
m.wait();
}
queue.remove(0);
m.notifyAll();
}
}
需要注意的是這里使用的都是notifyAll。原因是notify只會(huì)隨機(jī)喚醒一個(gè)等級(jí)集合中的線程,如果有兩個(gè)生產(chǎn)者,那么put中的notify喚醒的可能是另一個(gè)生產(chǎn)者,從而死鎖。因此put方法中應(yīng)當(dāng)使用notifyAll。同樣原因take方法中也應(yīng)當(dāng)使用notifyAll。
另外一個(gè)需要注意的點(diǎn)是這里新建了一個(gè)對(duì)象m,并使用m的wait和notify方法來(lái)實(shí)現(xiàn)同步,既然Java中任何對(duì)象都繼承了Object對(duì)象,那么BlockQueue這個(gè)類本身也是有wait和notify方法的,能否直接使用this.wait和this.notify并且在put和take方法上加synchronize呢?
答案是可以的。但是wait方法過(guò)程中會(huì)解除對(duì)象的監(jiān)視器鎖,從而會(huì)造成一些對(duì)synchronize的語(yǔ)義的干擾。
比如下面的代碼
public class ReenterSync {
public synchronized void reEnterSync() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " enter sync");
notify();
System.out.println(Thread.currentThread().getName() + " leave sync");
}
public synchronized void syncWait()
throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " enter sync");
wait();
System.out.println(Thread.currentThread().getName() + " leave sync");
}
public static void main(String[] args) {
ReenterSync reenterSync = new ReenterSync();
new Thread(() -> {
try {
reenterSync.syncWait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
reenterSync.reEnterSync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
------------------
Thread-0 enter sync
Thread-1 enter sync
Thread-1 leave sync
Thread-0 leave sync
wait()方法會(huì)導(dǎo)致對(duì)象的監(jiān)視器被解鎖,從而導(dǎo)致有兩個(gè)線程同時(shí)進(jìn)入同一個(gè)對(duì)象的不同synchronize方法。這會(huì)造成不必要的困擾。因此使用wait和notify來(lái)實(shí)現(xiàn)同步時(shí)通常會(huì)使用獨(dú)立的對(duì)象。