等待/通知机制
什么是等待/通知机制?
举例说明,厨师和服务员之间的交互:
1. 厨师做完一道菜的时间不确定,所以厨师将菜品放到”菜品传递台”上的时间也不确定;
2. 服务员取到菜的时间取决于厨师,所以服务员就处于等待状态;
3. 服务员如何取到菜呢?又得取决于厨师,厨师将菜放在”菜品传递台”上,其实就相当于一种通知,这时服务员才可以拿到菜并交给就餐者。
如何实现等待/通知机制?
一句话总结:wait 使线程停止运行,而 notify 使停止的线程继续运行。
wait() 方法
- 是 Object 类的方法,作用:使当前执行代码的线程进行等待,直到接到通知或被中断为止。
- 执行之后,当前线程释放锁。
- 只能在同步方法或同步块中调用 wait()方法。原因:JDK 强制的,方法调用之前必须先获得该对象的对象级别锁,如果调用时没有持有适当的锁,则抛出 IllegalMonitorStateException 异常。
notify()/notifyAll() 方法
- Object 类的方法,用来通知那些可能等待该对象的对象锁的其他线程。
- 在执行 notify() 方法后,当前线程不能马上释放该对象锁,wait 状态的线程也不能马上获取该对象锁,需要等到执行 notify() 方法的线程执行完(退出 synchronized 代码块后)。
- 只能在同步方法或同步块中调用。原因如上。
线程状态切换
Runnable 状态 & Running 状态
Runnable:就绪状态,随时可能被 CPU 调度执行
Running:运行状态,线程获取 CPU 权限进行执行
- 创建一个新的线程对象后,调用 start() 方法,系统为此线程分配 CPU 资源,线程进入 Runnable 状态
- 如果线程抢占到 CPU 资源,此线程进入 Running 状态
Running 状态 -> Blocked 状态
blocked:阻塞状态,线程放弃了 CPU 使用权,暂时停止运行,直到线程进入就绪状态。分为三种:
1. 等待阻塞:调用 wait()方法,让线程等到某工作的完成
2. 同步阻塞:synchronized 获取对象锁失败(可能锁被占用)
3. 其他阻塞:调用 sleep() | join() | 发出 IO 请求时,线程进入阻塞。
- 线程调用 sleep() 方法,主动放弃占用的处理器资源
- 线程调用了阻塞式 IO 方法,在该方法返回前,该线程被阻塞
- 线程试图获得一个同步监视器,但该监视器正被其他线程所持有
- 线程调用 wait() 方法,等待某个通知
- 程序调用了 suspend 方法将该线程挂起。(此方法容易死锁,避免使用)
Blocked 状态 -> Runnable 状态
- 调用 sleep() 方法后,sleep()超时
- 线程调用的阻塞 IO 已经返回,阻塞方法执行完毕
- 线程成功获得了试图同步的监视器
- 线程正在等到通知,其他线程发出了通知(notify() | notifyAll)
- 处于挂起状态的线程调用了 resume() 恢复方法
Dead 状态
线程执行完了或者异常退出了 run() 方法,结束生命周期。
每个锁对象都有两个队列:
- 就绪队列:存储将要获得锁的线程,一个线程被唤醒后,进入就绪队列,等到 CPU 调度
- 阻塞队列:存储被阻塞的线程,一个线程被 wait() 后,进入阻塞队列,等待下一次被唤醒
wait/notify模式的注意事项
- wait 释放锁,notify 不释放锁
- 当线程处于 wait 状态时,使用 interrupt() 方法会出现 InterruptedException 异常
- wait(long) 方法的作用:等待某一个时间内是否有线程对锁进行唤醒,如果超过时间则自动唤醒。
- 如果通知过早,则会打乱程序正常的运行逻辑。(wait 状态的线程不会被通知)
- wait 等待的条件发生变化,也容易造成程序逻辑的混乱
经典案例:生产者/消费者模式实现
实战:等待/通知之交叉备份
创建 20 个线程,其中 10 个线程是将数据备份到 A 数据中,另外 10 个线程是将数据备份到 B 数据库中,并且备份 A 数据库和 B 数据库是交叉进行的。
public class DBTools {
// 确保备份 "★★★★★" 首先执行,然后与 "☆☆☆☆☆" 交替进行备份
volatile private boolean prevIsA = false;
synchronized public void backupA() {
try {
while (prevIsA) {
wait();
}
for (int i = 0; i < 5; i++) {
System.out.println("★★★★★");
}
prevIsA = true;
notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized public void backupB() {
try {
while (!prevIsA) {
wait();
}
for (int i = 0; i < 5; i++) {
System.out.println("☆☆☆☆☆");
}
prevIsA = false;
notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class BackupA extends Thread {
private DBTools dbTools;
public BackupA(DBTools dbTools) {
super();
this.dbTools = dbTools;
}
@Override
public void run() {
dbTools.backupA();
}
}
public class BackupB extends Thread {
private DBTools dbTools;
public BackupB(DBTools dbTools) {
super();
this.dbTools = dbTools;
}
@Override
public void run() {
dbTools.backupB();
}
}
public class Run {
public static void main(String[] args) {
DBTools dbTools = new DBTools();
for (int i = 0; i < 20; i++) {
BackupB output = new BackupB(dbTools);
output.start();
BackupA input = new BackupA(dbTools);
input.start();
}
}
}