Wait&Notify
以下内容来自《Java并发编程的艺术》,4.3.2 等待/通知机制
线程A等待某一个变量_v_满足某个条件,而线程B会在某个不确定的时刻修改_v_,以使其满足条件,那么线程A所要做的无非以下两种操作:
- 轮询变量_v_,直到_v_满足条件,A继续完成它的工作
- 每隔一段时间检查变量_v_,这期间可能休眠,也可能做其它的事
可以看到,以上两种操作刚好是矛盾的,第一种会一直占用CPU资源,而且是在浪费,但是可以保证实时性,即当_v_满足条件,它立刻就会知道。第二种会降低CPU的开销,或者减少浪费,但是很难保证实时性。
Java内置的等待/通知机制能够很好的解决这个矛盾并实现所需的功能。
等待/通知相关的方法是任意Java对象都具有的,这些方法定义在java.lang.Object
上
Method | Desc |
---|---|
notify() | |
notifyAll() | |
wait() | |
wait(long) | |
wait(long, int) |
JDK中关于这几个方法的解释:
public final native void notify()
唤醒一个在该对象监视器上等待的线程。如果有很多线程在该对象上等待,就挑选其中的一个。选择是任意的,在具体实现的时候自由裁决。一个线程通过调用该对象的wait
方法,开始在该对象的监视器锁上等待(前提是该线程持有该监视器锁)。
被唤醒的线程并不是立即开始执行,要等到当前线程释放掉该对象锁(或从同步块中出来)。被唤醒的线程将以常规的方式继续在该对象同步块中完成它的工作。例如,唤醒的线程对于接下来要获得锁的线程没有优先级。
该方法应该被持有该对象监视器的线程调用。有三种方式使一个线程拥有对象监视器:
- 执行该对象的同步方法
- 进入同步代码块
- 对于类对象,执行类的静态同步方法
同一时刻只有一个线程能持有对象监视器。
Throws:IllegalMonitorStateException
如果当前线程不持有该对象的监视器
public final native void notifyAll()
唤醒所有在该对象锁上等待的线程。即所有线程都开始准备进入该对象监视器。
public final native void wait(long timeout) throws InterruptedException
引起当前线程等待,直到其它线程调用该对象的notify()
或notifyAll()
方法,或者在特定的超时时间之后。
当前线程需要持有该对象的监视器。
该方法会引起当前线程(如线程T),使其放弃持有的对象锁,将自身放入该对象的等待集合。线程T不能被线程调度器调度,会保持挂起直到如下发生:
- 某个其它线程调用该对象的
notify()
方法,线程T可能会被随机选中,并唤醒 - 其它线程调用该对象的
notifyAll()
方法 - 其它线程中断线程T
- 指定的超时时间到了。如果超时时间设为0,那么超时时间无效,结果等价于
wait()
方法
然后,线程T会从该对象的等待集合中移除,然后再次可被调度。然后它会再次与其它线程竞争进入对象监视器的机会,一旦它取得了控制权,它再次回到它睡眠前的状态,就好像没有发上一样,继续执行。
线程可以不需要通知,中断或超时而被唤醒,这里叫做伪唤醒。尽管实际很少发生,程序必须小心测试唤醒线程的条件,而且当条件不满足的时候,换句话说,应该让wait()
方法在一个循环中执行,直到条件满足,就像这样:
synchronized(obj){ while(){ obj.wait(); } //do bussiness}
如果当前线程在等待之前,或正在等待的时候被中断了,就抛出InterruptedException
异常。这个异常会知道线程再次获得锁的时候才抛出。
当该线程在等待的时候,它还是有可能持有其它的对象锁!这里一定要注意。
Throws
IllegalArgumentException
:timeout参数为负值
IllegalMonitorStateException
:如果当前线程没有持有该对象锁
InterruptedException
:如果其它线程在该线程开始等待或正在等待的时候中断线程,中断标志位会清除。
来个图:
总结如下过程:
time:01 WaitThread NotifyThreadtime:02 进入监视器 ...time:03 ... 进入监视器time:04 ... ... time:05 进入成功 ...time:06 ... 进入失败time:07 ... ...time:08 ... 进入同步队列time:09 Object.wait() ...time:10 ... 从同步队列出来time:11 进入等待队列 进入监视器time:12 ... ...time:13 ... 进入成功time:14 ... ...time:15 ... Object.notifytime:16 ... ...time:17 从等待队列进入同步队列 ...time:18 ... 出监视器time:19 进入监视器 ...time:20 ... 结束time:21 进入成功time:22 ...time:23 ...time:24 结束
过程就是这么个过程,纵向为时间。
等待/通知最佳实践
等待方遵循原则:
- 获取对象锁
- 如果条件不满足,那么调用对象的
wait()
方法,被通知后仍要检查条件 - 条件满足就执行对应的逻辑
对应的伪代码:
synchronized(obj){ while( <条件不满足> ){ obj.wait(); } //执行相应逻辑} 条件不满足>
通知方伪代码:
- 获得对象锁
- 改变条件
- 通知所有在该对象的线程(可能会通知到业务不相关的线程)
对应伪代码:
sychronized(obj){ //改变条件 obj.notifyAll();}