【多线程】wait 和 notify
合理的协调多个线程之间的执行先后顺序.

目录
由于线程之间是抢占式执行的(线程在操作系统上的调度是随机的), 因此线程之间执行的先后顺序难以预知. 但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.多个线程,需要控制线程之间执行某个逻辑的先后顺序,就可以让后执行的逻辑,使用 wait,先执行线程,完成某些逻辑之后,通过 notify 唤醒对应的 wait.
一、线程饿死问题
针对上述问题,同样也可以使用 wait/notify 来解决。首先让1号滑稽,拿到锁的时候进行判定
判定当前能否执行 "取钱" 操作。如果能执行,就正常执行。如果不能执行呢,就需要主动释放锁并且 "阻塞等待"(通过调用 wait),此时这个线程就不会在后续参与锁的竞争了,一直阻塞到 "取钱" 的条件具备了,此时,再由其他线程通过通知机制(notify)唤醒这个线程。
二、wait() 方法
wait 做的事情
▪️使当前执行代码的线程进行等待. (把线程放到等待队列中)
▪️释放当前的锁.
▪️满足⼀定条件时被唤醒, 重新尝试获取这个锁.
public class Demo23 {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
System.out.println("wait 之前");
obj.wait();
System.out.println("wait 之后");
}
}

非法的锁状态异常:意思是,调用 wait 的时候当前的锁的状态(加锁状态、未加锁状态),是非法的(不正确的)。wait 中,会进行一个操作,就是针对 obj 对象,先进行解锁,所以,使用 wait,务必要放到 synchronized 代码块里面,必须得先加上锁,才能谈 "解锁"。就像滑稽1,进入ATM之后,发现没钱,就要阻塞等待。而进行阻塞等待,一定是,先释放锁,再等待,如果他抱着锁等待,等待也没法把机会让给别人。wait既然要释放锁,前提就必须是先加上锁。
📢wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.
public class Demo23 {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
System.out.println("wait 之前");
synchronized (obj) {
obj.wait();
}
System.out.println("wait 之后");
}
}


wait 结束等待的条件
🔹其他线程调用该对象的 notify 方法.
🔹wait 等待时间超时( wait 方法提供一个带有 timeout 参数的版本,来指定等待时间).
🔹其他线程调用该等待线程的 interrupted 方法,导致 wait 抛出 InterruptedException 异常.

这样在执行到 object.wait() 之后就⼀直等待下去,那么程序肯定不能⼀直这么等待下去了。这个时候就需要使用到了另外⼀个方法,唤醒的方法 notify()。
三、notify() 方法
wait 使调用的线程进入阻塞,notify 则是通知 wait 的线程被唤醒(另一个线程调用的)。被唤醒的wait 就会重新竞争锁,并且在拿到锁之后,再继续执行。
回顾 wait 一共做了三件事:
(1)释放锁
(2)进入阻塞等待,准备接受通知
(3)收到通知之后,被唤醒,并且重新尝试获取锁
🍬方法 notify() 也要在同步方法或同步块中调用(在 notify() 中,也需要确保先加锁才能执行 ),该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知 notify,并使它们重新获取该对象的对象锁
import java.util.Scanner;
public class Demo24 {
private static Object locker = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (locker) {
System.out.println("t1 wait 之前");
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1 wait 之后");
}
});
Thread t2 = new Thread(() -> {
synchronized (locker) {
System.out.println("t2 notify 之前");
Scanner sc = new Scanner(System.in);
sc.next();//此处用户输入啥都行,主要是通过这个 next ,构造“阻塞”
synchronized (locker){
locker.notify();
}
System.out.println("t2 notify 之后");
}
});
t1.start();
t2.start();
}
}

🍪如果有多个线程等待,则由线程调度器随机挑选出⼀个呈 wait 状态的线程。(并没有 "先来后到")
import java.util.Scanner;
public class Demo25 {
private static Object locker = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (locker) {
System.out.println("t1 wait 之前");
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 wait 之后");
}
});
Thread t2 = new Thread(() -> {
synchronized (locker) {
System.out.println("t2 wait 之前");
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 wait 之后");
}
});
Thread t3 = new Thread(() -> {
synchronized (locker) {
System.out.println("t3 wait 之前");
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3 wait 之后");
}
});
Thread t4 = new Thread(() -> {
Scanner sc = new Scanner(System.in);
sc.next();
System.out.println("t4 notify 之前");
synchronized (locker) {
locker.notify();
}
System.out.println("t4 notify 之后");
});
t1.start();
t2.start();
t3.start();
t4.start();
}
}

🍦在 notify() 方法后,当前线程不会马上释放该对象锁,要等到执行 notify() 方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁
四、notifyAll() 方法
notify 方法只是唤醒某⼀个等待线程. 使用 notifyAll 方法可以⼀次唤醒所有的等待线程.
注意: 虽然是同时唤醒 3 个线程, 但是这 3 个线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的执行.
💿唤醒是唤醒一个,还是唤醒所有,大部分的情况,使用唤醒一个的。一个一个唤醒(多执行几次 notify )整个程序执行过程是比较有序的,如果一下唤醒所有,这些被唤醒的线程,就会无序的竞争锁
📀如果对方没有线程 wait,或者只有一个线程 wait,但是另一个线程 notify 多次,会咋样呢?不会咋样,notify 通知的时候,如果无人 wait,不会有任何副作用
import java.util.Scanner; public class Demo25 { private static Object locker = new Object(); public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (locker) { System.out.println("t1 wait 之前"); try { locker.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1 wait 之后"); } }); Thread t2 = new Thread(() -> { synchronized (locker) { System.out.println("t2 wait 之前"); try { locker.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t2 wait 之后"); } }); Thread t3 = new Thread(() -> { synchronized (locker) { System.out.println("t3 wait 之前"); try { locker.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t3 wait 之后"); } }); Thread t4 = new Thread(() -> { Scanner sc = new Scanner(System.in); sc.next(); System.out.println("t4 notify 之前"); synchronized (locker) { locker.notify(); locker.notify(); locker.notify(); locker.notify(); locker.notify(); locker.notify(); locker.notify(); locker.notify(); locker.notify(); locker.notify(); } System.out.println("t4 notify 之后"); }); t1.start(); t2.start(); t3.start(); t4.start(); } }
五、📝面试题: wait和sleep的对比
其实理论上 wait 和 sleep 完全是没有可比性的,因为⼀个是用于线程之间的通信的,⼀个是让线程阻塞⼀段时间,唯⼀的相同点就是都可以让线程放弃执行⼀段时间.
当然为了⾯试的目的,我们还是总结下:
1️⃣wait 需要搭配 synchronized 使用. sleep 不需要.
2️⃣wait 是 Object 的方法 sleep 是 Thread 的静态方法.
wait 默认也是 "死等",wait 还提供带参数的版本,指定超时时间。如果 wait 达到了最大的时间,还没有 notify 就不会继续等待了,而是直接继续执行。wait(1000) 和 sleep (1000) 看起来就有点相似了,wait 和 sleep 还是有本质区别的,使用 wait 的目的是为了提前唤醒。sleep 就是固定时间的阻塞,不涉及到唤醒的。虽然 sleep 可以被 Interrupt 唤醒,但 Interrupt 操作,表示的意思不是 "唤醒" 而是要终止线程了。wait 必须要搭配 synchronized 使用,并且 wait 会先释放锁,同时进行等待,而 sleep 和锁无关,如果不加锁,sleep 可以正常使用,如果加了锁,sleep 操作不会释放锁而是 "抱着锁" 一起睡,其他线程无法拿到锁的。
共同点:
都是使线程暂停一段时间的方法。
不同点:
- wait 是 Object 类中的一个方法,sleep 是 Thread 类中的一个方法;
- wait 必须在 synchronized 修饰的代码块或方法中使用,sleep 方法可以在任何位置使用;
- wait 被调用后当前线程进入 BLOCK 状态并释放锁,并可以通过 notify 和 notifyAll 方法进行唤醒;sleep 被调用后当前线程进入 TIMED_WAIT 状态,不涉及锁相关的操作;
六、练习
①
创建三个线程,分别打印A,B,C 通过 wait、notify 约定线程的打印顺序,先打印A,然后B,最后C
直接创建线程
public class Demo26 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("A");
});
Thread t2 = new Thread(() -> {
System.out.println("B");
});
Thread t3 = new Thread(() -> {
System.out.println("C");
});
t1.start();
t2.start();
t3.start();
}
}

通过 wait、notify
public class Demo26 {
private static Object locker1 = new Object();
private static Object locker2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("A");
synchronized (locker1) {
locker1.notify();
}
});
Thread t2 = new Thread(() -> {
synchronized (locker1) {
try {
locker1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("B");
synchronized (locker2) {
locker2.notify();
}
});
Thread t3 = new Thread(() -> {
synchronized (locker2) {
try {
locker2.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("C");
});
t1.start();
t2.start();
t3.start();
}
}

这个代码中,大概率是先执行 t2 的 wait,后执行 t1 的 notify,因为t1的打印需要消耗不少时间,代码的逻辑是一切顺利的。但是实际上,存在这样的概率,t1 先执行了打印和 notify,然后 t2 才执行 wait,意味着 t1 执行完打印的通知来的早了,t2 错过了,t2 的 wait 就无人唤醒了,只需要在 t1 中加上 sleep。

②
有三个线程,分别只能打印A,B和C
要求按顺序打印ABC,打印10次
输出示例:
ABC
ABC
ABC
ABC
ABC
ABC
ABC
ABC
ABC
ABC
public class Demo {
private static Object locker1 = new Object();
private static Object locker2 = new Object();
private static Object locker3 = new Object();
public static void main(String[] args) throws InterruptedException {
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) {
e.printStackTrace();
}
});
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) {
e.printStackTrace();
}
});
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) {
e.printStackTrace();
}
});
t1.start();
t2.start();
t3.start();
Thread.sleep(1000);
// 从线程 t1 启动
synchronized (locker1) {
locker1.notify();
}
}
}更多推荐


所有评论(0)