Object对象方法之wait与notify
本文首发于https://www.jianshu.com/p/18769b7dc46f
道生一,一生二,二生三,三生万物。
老子《道德经》
Java是单继承模型,有类似于老子的道德经的哲学,所有的类最终都会继承自一个原始类,这个类就是Object
。
Object
对象中总共有11个可供protect或public方法。除去toString
方法用于生成类的可读化表示外,这些方法可以按其用途分为以下四类:
- 类的表示及反射
getClass
- 类与Map的联动
equals
,hashCode
- Java的同步抽象
wait
(三个重载),notify
,notifyAll
- Java中对象内存表示及垃圾收集
finalize
,clone
本文结合Java语言规范 (Java 8) 介绍wait
和notify
方法的规范及其在同步中的应用。
方法定义及规范
Object
类中关于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
方法的两个不同的重载都是wait(timeout)
方法的辅助方法,值得一提的是方法wait(long timeout, int nanos)
中主动放弃了纳秒级精度。由于常用的计算机系统都不是硬实时系统,因此参数中的timeout
都只能是一个粗略值。
Java语言规范中关于wait
和notify
的部分位于第17章,这一章主要介绍Java的内存模型,同步语义及抽象。
规范中关于wait
方法的定义如下
假定线程t
在对象m
上执行方法wait
, n
为t
执行的lock次数。
+ 如果n == 0
,即t
还未获得对象m的锁,那么会抛出IllegalMonitorStateException
, 这也就是为什么wait
与notify
方法需要在sychronzied
段中执行。
+ 如果t
被中断,那么会抛出InterruptedException
且t
的中断状态会被设置成false
否则
1. 线程t
会被加入对象m
的等待集合,并且在对象m上执行n
次unlock操作。
2. t
将不会执行任何后续的指令,直到它从m
的等待集合中移除。
线程会因为以下五种原因从等待集合中移除,并在之后的某个时间恢复运行。
m.notify
m.notifyAll
t.interrupt
timeout
超时spurious wake-ups
。JVM允许此方法实现为随机唤醒,因此处于wait
状态的线程可能没有任何原因就被唤醒,这也是为什么使用wait
方法时需要将其放在循环体中的原因。
t
在对象m
上执行n
次lock
操作。- 如果
t
是由于调用t.interrupt
从等待集合中移除,那么t
的中断状态会置为false
规范中关于notify
和notifyAll
方法的定义如下:
线程t
,对象m
,n
为t
在m
上执行的lock次数。
- 如果
n == 0
, 则抛出IllegalMonitorStateException
- 如果
n > 0
, 执行notify
会将m
的等待集合中某个线程移除。 - 如果
n > 0
, 执行nofityAll
会将m
的等待集合中的所有线程移除。
规范中关于中断的定义如下:
线程中断有以下两种方法来执行
Thread.interrupt
ThraedGroup.interrupt
假设线程t
执行线程u
的中断方法u.interrupt
(t
和u
可能是同一个线程),这个调用会使得t的interruption状态变成true
.
如果u
处于某个对象m
的等待集合中,这将会使得u
从wait
方法中恢复,并且抛出InterruptedExcetion
可以通过Thread.isInterrupted
方法来判断一个方法是否处于中断状态。Thread.interrupted
静态方法可用于一个线程获得它的中断状态并清空中断状态。
JVM字节码
分别查看以下wait
方法和notify
方法对应的字节码,结果如下:
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
...
从字节码中可以看出,wait
和notify
功能由JVM的实现,对应的字节码只是获得相应的监视器锁,并执行相应的native方法。
代码实例分析
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
}
根据JVM规范,若线程没有获得obj
的监视器锁,即规范中n == 0
的情况下,会抛出IllegalMonitorStateException
。
为什么需要在执行wait
和notify
方法时先获得对象锁呢?从规范中可以看出,wait
和notify
操作需要对对象的等待集合进行更改,而这两个更改本身就是竞态条件,因此需要同步。
在JVM的wait
方法的实现中,需要释放已经获得对象监视器锁,从而允许执行notify
的代码段获得锁并执行。
2. 事件有序
interrupt
与notify
事件必然以一定顺序发生,一个在wait
中的线程,同时被另外两个线程notify
和interupt
时,线程要么被中断,且通知被另外一个等待的线程获取;要么线程先听到通知恢复执行,未抛出InterruptException
,线程的interuptted状态变成true
。
可以由以下代码验证:
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
- 生产者和消费者问题
并发问题的一个经典场景是生产者和消费者的问题,有多个线程生产,另外多个线程消费。应对这个问题通常会使用阻塞队列。
假定队列的接口是put
和take
,put
用于生产,take
用于消费。非线程安全的queue
用于存放元素。使用对象m
用的wait
和notify
方法来实现同步。
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只会随机唤醒一个等级集合中的线程,如果有两个生产者,那么put
中的notify唤醒的可能是另一个生产者,从而死锁。因此put
方法中应当使用notifyAll
。同样原因take
方法中也应当使用notifyAll
。
另外一个需要注意的点是这里新建了一个对象m,并使用m的wait
和notify
方法来实现同步,既然Java中任何对象都继承了Object对象,那么BlockQueue这个类本身也是有wait和notify方法的,能否直接使用this.wait
和this.notify
并且在put
和take
方法上加synchronize
呢?
答案是可以的。但是wait方法过程中会解除对象的监视器锁,从而会造成一些对synchronize的语义的干扰。
比如下面的代码
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()方法会导致对象的监视器被解锁,从而导致有两个线程同时进入同一个对象的不同synchronize方法。这会造成不必要的困扰。因此使用wait
和notify
来实现同步时通常会使用独立的对象。