1. 协调多个线程之间的执行先后顺序的方法介绍
由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知;但是实际开发中,有时候我们希望合理地协调多个线程之间的执行先后顺序。
拓展: wait() 和 sleep() 的区别
wait() 和 sleep()都是用于暂停线程的操作,但它们有明显的区别(先说面试官最关心的):
(1)使用要求不同
- wait() 必须在同步块或同步方法内调用(嵌套一层 synchronized ),否则会抛出IllegalMonitorStateException。
- 这是因为 wait() 依赖于对象锁来管理线程的等待和唤醒机制。
- 调用后,当前线程会释放它持有的对象锁,并进入等待状态。
- sleep()方法可以在任何上下文中调用,不需要获取对象锁。
- 调用后,线程会进入休眠状态,但不会释放它持有的任何锁。
- 所以如果 wait() 和 sleep() 都嵌套一层锁,分别被唤醒时,wait() 会释放锁,而 sleep() 不会释放锁;
(2)方法所属类不同
- wait() :属于 Object 类的非静态方法。
- sleep() :属于 Thread 类的静态方法。
(3)恢复方式不同
- wait() 需要被其他线程通过 notify() 或 notifyAll() 显式唤醒;
- 或者被 wait(long timeout) 的超时参数唤醒。
- sleep() 在指定时间后自动恢复运行,或通过 interrupt() 提前唤醒,抛出 InterruptedException 异常。
(4)用途不同
- wait() 通常用于线程间通信,配合 notify() 或 notifyAll() 来实现线程的协调工作。
- sleep() 用于让线程暂停执行一段时间,通常用于控制线程的执行频率或模拟延时。
(5)常见错误
误用sleep() :
有时开发者会错误地使用 sleep() 进行线程间通信,但是 sleep() 不释放锁,可能会导致其他线程无法进入同步块,造成线程饥饿或死锁。
忽略中断:
sleep() 可能抛出 InterruptedException , 如果不正确处理中断信号,可能会导致线程提前退出或错误行为。
2. wait()
2.1 线程饿死
如上图,鸟妈妈(CPU)抓虫(调度资源)喂小鸟(线程),就是一个典型的 “线程饿死” 情景:
- 对于线程饿死,并不是鸟妈妈把捉到的虫全都喂一只鸟宝宝,其他鸟宝宝一点都吃不到,而是鸟妈妈把捉到的虫子,绝大多数都喂给了一个鸟宝宝,剩下的鸟宝宝只能吃到一点点;
- 使用 wait(),notify() 就是为了优化 “鸟妈妈把大多数的虫子,都分给一只鸟宝宝” 这一行为。
2.2 调用 wait()
- wait() 和 notify 都是Object 的方法;Java 中的任意对象都提供了 wait() 和 notify();
- wait() 能使当前执行代码的线程进行等待(把线程放到等待队列中);
- wait() 一被调用,就会释放当前的锁;
- 满足一定条件时被唤醒,重新尝试获取这个锁;
- 注意:在判断是否满足唤醒条件时,我们可以把 if(判断条件) 改成 while(判断条件) ,这样可以避免被 interrupt() 类似的方法非法打断。在该文章模拟阻塞队列的 put() 和 take() 有详细解释
- wait 要搭配 synchronized 来使用;脱离synchronized,使用 wait 会直接抛出上述异常;
- 上述异常被抛出的本质,是针对未加锁的锁对象进行释放锁操作;
2.3 唤醒 wait()
- 其他线程调用该对象的 notify() ;
- wait() 等待时间超时(wait() 提供一个带有 timeout 参数的版本,来指定等待时间);
- 其他线程调用该等待线程的 interrupted(),导致 wait() 抛出 InterruptedException 异常;
- 在synchronized的代码块中,等到wait() 结束,wait后面到 }的部分,还有一些其他的逻辑;
- 这些逻辑还是期望在锁的范围内进行调度,所以 wait 后面到}的部分也要嵌套在锁内,wait() 被唤醒后,会重新对这些逻辑上锁,以保证线程安全。
3. notify()
3.1 wait() 和 notify() 的需要同一个对象调用
- 通过相同的对象调用 wait() 和 notify() ,是两个线程沟通的桥梁;
- wait() 和 notify() 针对同一个对象才能生效;如果是不同对象,则没有任何相互的影响和作用~
- 为了验证这一点,外面写出如下代码:
代码逻辑:
- 在 t1 线程执行到第一个打印日志之后,执行 wait() ,此时就需要通过 t2 线程来唤醒 t1;
- 我们先用 Scaner 来阻塞 t2,这样操作就可以手动控制 t2 对 t1 的唤醒;
- 在输入内容后,t2执行 notify(),如果调用 wait() 和 notify() 的两个对象相同,则 t1 会成功被唤醒。
上图是不同对象调用 wait() 和 notify() 的情况,我们再来看看相同对象调用的结果:
3.2 notify() 要同步方法或同步块中调用
和 wait() 一样,也是要在同步方法或同步块(嵌套一层 synchronized)中调用;
3.3 notify() 随机唤醒多个 wait() 中的其中一个
- notify()通知正在 wait(), 等待同一个对象锁的其它线程,对其它线程中的一个线程,发出通知notify,并使这个线程重新获取该对象的对象锁;
- 如果有多个线程等待,则有线程调度器随机挑选出一个呈wait状态的线程。(并没有"先来后到");
- 我们来看程序运行结果,在输入任意内容后,打印的结束日志是不一样的;
- 这就证明了线程调度器随机挑选出一个呈 wait 状态的线程;
- 有两个 wait(),一个 notify(),一定有一个线程没有被唤醒,导致整个进行无法结束;
- 既然有两个 wait(),我们就设置两个 notify() 即可解决该问题;
- 在 notify()方法后,当前线程不会马上释放该对象锁;
- 要等到执行 notify() 所在同步代码块退出之后,才会释放对象锁 ;
4. notifyAll()
- 对于刚刚上面写的代码,只有一个 notify(),就只能唤醒一个 wait():
- 因此,我们可以考虑用 notifyAll(),唤醒所有相同对象调用 wait() 的线程:
- 虽然同时唤醒了 t1 和 t2,但是由于 wait() 被唤醒之后,要重新加锁;因此其中某个线程,先加上锁,开始执行,而另一个线程因为加锁失败,再次阻塞等待;
- 等到先加锁的线程解锁了,后加锁的线程才能加上锁,而继续执行~
总结
- 因为这个原因,notifyAll() 在实际开发中,虽然可以唤醒所有 wait() ,但是用的并不多。
- 因为不是一口气全部唤醒 wait(),而是每次唤醒其中一个线程,通过多次唤醒,把所有 wait()状态的线程唤醒;
- notifyAll() 在唤醒其中一个 wait() 状态的线程时,其他线程依旧因为 wait() 尝试重新获取锁对象,而陷入阻塞等待;
- 比起唤醒所有,我们更希望通过一个一个的 notify() 精确唤醒每一个线程。
5. 应用 wait() 和 notify() 解决编程题
5.1 题目
5.2 程序运行结果
5.3 完整代码
package Thread;
public class Demo29 {
public static void main(String[] args) throws InterruptedException {
Object locker1 = new Object();
Object locker2 = new Object();
Object locker3 = new Object();
Thread t1 = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
synchronized (locker1) {
locker1.wait();
}
System.out.print("A");
synchronized (locker2){
locker2.notify();
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread t2 = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
synchronized (locker2) {
locker2.wait();
}
System.out.print("B");
synchronized (locker3){
locker3.notify();
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread t3 = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
synchronized (locker3) {
locker3.wait();
}
System.out.println("C");
synchronized (locker1){
locker1.notify();
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t1.start();
t2.start();
t3.start();
//轮子已经造好了,现在需要推一把,让轮子转起来
//需要确保上述主线程都执行到 wait(),再推轮子
Thread.sleep(1000);
synchronized (locker1){
locker1.notify();
}
}
}