前言
有时候,各个线程在执行过程需要相互通信,例如发送控制指令、向另一个暂停执行的线程发送恢复执行的指令。
如果只是发送一个控制指令,可以使用一个用 volatile 修饰的共享标记量在两个线程之间通信。如果要唤醒另一个暂停执行的线程,就涉及到了 wait/notify 机制,下面就来分析一下这个机制。
wait 和 notify 相关方法
锁本质上还是一个对象,在所有对象的父类 Object 中定义了以下几个方法:1
2
3
4
5
6public final void wait() throws InterruptedException
public final void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
public final void notify();
public final void notifyAll();
这几个方法的作用如下:
方法名 | 说明 |
---|---|
wait() | 线程获取到锁后,调用锁对象的该方法,线程释放锁并加入到与锁对象关联的等待队列 |
wait(long timeout) | 与 wait() 相似,不过在等待指定的毫秒数后,自动将线程移除出等待队列 |
wait(long timeout, int nanos) | 与上面一样,只是时间粒度更小,即指定的毫秒数加上纳秒数 |
notify() | 通知(唤醒)一个与锁对象关联的等待队列的线程,使它从 wait 方法中返回并继续往下执行 |
notifyAll() | 与上面类似,只不过是唤醒等待队列的所有线程 |
使用场景
那么要在什么时候使用 wait/notify 呢?通常情况如下:
一个线程在获取到锁后,如果指定条件不满足,应该主动让出锁,并到指定区域等待,直到某个线程完成了指定条件后,再通知(唤醒)这个等待的线程,让它继续往下执行。
通用模式
了解了它的使用场景后,下面看下它是怎么用的:
通常有两个线程,一个是等待线程,另一个是通知线程。
等待线程的执行步骤如下:
- 获取对象锁
- 如果指定条件不满足,就调用锁对象的 wait 方法,被唤醒后要再次检查条件,所以在判断条件时使用 while 而不是 if。
- 条件满足后继续往下执行
通用代码如下:1
2
3
4
5
6
7synchronized (对象) {
处理逻辑(可选)
while (不满足指定条件) {
对象.wait();
}
处理逻辑(可选)
}
通知线程的执行步骤如下:
- 获得对象锁
- 完成指定条件
- 通知(唤醒)等待队列中的线程
通用代码如下:1
2
3
4synchronized (对象) {
完成指定条件
对象.notifyAll();
}
注意事项
- 必须在同步代码块中调用 wait、notify 或 notifyAll 方法,否则会抛出 IllegalMonitorStateException 异常。
因为必须要保证判断条件、调用 wait 方法,以及完成条件、调用 notify 方法都是原子性操作。不然可能会出现这样的情况:等待线程在判断完条件不满足后,还未执行 wait 方法时,突然切换到了通知线程,通知线程完成条件并调用了 notify 方法,这时再切换回等待线程,继续执行 wait 方法,这时 wait 方法就会一直等待下去。
- 在同步代码块中,只能调用获取的锁对象的 wait、notify 或 notifyAll 方法,否则会抛出 IllegalMonitorStateException 异常。
这是因为如果当前线程不持有某个对象的锁,它就不能调用该对象的 wait 方法来让出该锁,同理,也不能调用该对象的 notify 方法来唤醒相应等待队列的线程。
- 在调用完锁对象的 notify 或 notifyAll 方法后,等待线程并不会立即从 wait 方法返回,需要等到通知线程执行完毕并释放锁后,等待线程才能获取到锁并从 wait 方法中返回。
wait 和 sleep 的区别
- wait 是 Object 的方法,而 sleep 是 Thread 的方法
- 调用 wait 方法需要先获得锁,而调用 sleep 不需要
- 调用 wait 方法后等待的线程需要 notify 来唤醒,而调用 sleep 方法后,线程会在指定等待时间后唤醒
- 线程在调用 wait 方法后会先释放锁,而调用 sleep 方法并不会释放锁