本文首發(fā)于http://www.reibang.com/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.notify
m.notifyAll
t.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ì)象风皿。