引言
wait和notify方法是线程之间协作的重要方法。可以后执行的代码等待先执行的代码跑完在执行。属于object类,不属于Thread类。下面我们就来详细说一下wait和notify的方法。
wait 方法:
wait 方法的使用可以让线程进入waiting 和 timedwaiting状态,这两个的区别 在于 wait 括号里面是否填写参数。wait括号里的参数代表着线程最多等待多长时间,超过这个时间线程就不再等待。
- wait 方法属于Object,任意对象都可使用,notify方法也一样
下面我们就来通过代码来加深对wait方法的理解
java
package Thread.WaitAndNotify;
public class Demo1 {
public static void main(String[] args) {
Object object = new Object();
Thread t1 = new Thread(()->{
System.out.println("t1线程wait之前");
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1线程wait之后");
});
t1.start();
}
}
代码分析:
- 首先我们发现wait方法抛出了一个中断异常,这就允许线程在等待的时候被外部中断,避免代码永久堵塞,其实任何关**于堵塞的方法都会抛出一个中断异常,**这也是java终止线程堵塞的一种方法
- 我们观察代码在wait之后有一个 打印。在 wait之后也有一个打印。那么是不是,正常来说代码会有两行打印。好的我们运行代码,我们发现结果并没有我们预料的一样
意外结果分析:

代码抛出了一个IllegalMonitorStateException的异常 。这个代表着非法监视器状态异常。那么监视器又是什么呢?这个其实在之前已经说过了,我们讲synchronized的时候已经提到过 synchronized也是monitor lock 那么我们知道了这个异常就是锁状态的异常!为什么会这样呢?
意外结果原因:
根本原因:wait在调用的时候会先释放锁。这样做是避免当线程永久等待的时候还占着锁,导致其他线程也永久堵塞。
wait释放锁初体验
java
package Thread.WaitAndNotify;
import java.util.Scanner;
public class Demo2 {
private static volatile boolean flg = false;
public static void main(String[] args) {
Object lock = new Object();
Thread t1 = new Thread(()->{
synchronized (lock){
while(!flg){
// try {
// lock.wait();
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
}
}
});
Thread t2 = new Thread(()->{
Scanner scanner = new Scanner(System.in);
scanner.next();
synchronized (lock){
flg = true;
lock.notify();
}
});
t1.start();
t2.start();
}
}
代码分析:
- 我们运行代码之后,正常来说t2由于scanner的堵塞作用,t1会加锁陷入循环,导致 t2一直得不到锁这就导致了代码陷入了死循环。
- 但是当我们在t1里面加上wait方法之后我们发现代码并没有陷入循环 原因就是wait方法释放了锁导致t2有机会获得锁
wait方法释放锁:
- 在线程执行了wait之后,该线程会处于阻塞状态,假如这个线程没有释放锁,那么在堵塞的时候他就会一直持有锁,导致其他线程由于没有锁而进入堵塞状态。
- 因此,java就强制要求我们在wait的时候要先释放锁,避免上述的情况,因此我们要在 synchorized里写wait方法
java
synchronized (lock){ //加锁
lock1.wait(); // 释放锁
//加锁
}//释放锁
- 观察上述代码,一次wait方法涉及两次加锁和释放锁 。第一次加锁是在 synchorized的第一个大括号,第一次释放锁,是执行到wait方法的时候释放
- 第二次加锁是执行wait之后,第二次释放锁是synchorized的最后一个大括号之后。
注意:synchorized的锁对象和wait的对象必须是同一个。此外wait方法不一定通过notity唤醒,也可以被异常提前终止
wait和join的方法的区别
- join方法也是让一个线程等待另一个线程的等另一个线程结束,但是join的等待,只有另一个线程执行完了,这个线程才能走
- wait的等待,可以通过notify提前唤醒,也就是说被等待的线程不一定要结束,等待的线程也能执行
补充:关于wait和sleep
- wait 也有一个带参数的版本,这个表示线程等待一段时间之后,即使没有notify他也会去执行,那么带参数的wait方法是不是和sleep方法很像,别急下面我们就是他们的不同和相同之处
- wait可以通过notify提前唤醒,sleep也可以通过interrupt唤醒,wait和sleep都有等待时间
- wait的使用需要和锁一块使用,sleep的使用则不需要和锁一块使用,如果在synchronized里面,wait会进行一次解锁,一次加锁,而sleep则不会
补充:线程饿死
假设由A B C线程共同竞争一把锁,此时A获得了锁,在A释放锁之后 A B C三个线程又重新竞争这一把锁。那这个时候就会存在一种状态A不断地获得锁,导致B C线程一直得不到锁,无法执行任务。那次是 B C就是线程饿死了。线程饿死:简单说就是 某个线程永远得不到它需要的资源(比如 CPU 时间、锁),导致一直无法执行,永远 "饿肚子"
线程饿死与死锁不同
- 死锁是由于锁地竞争导致所有线程都无法执行,卡住
- 饿死则是某一个线程持续不断地得到锁,导致其他线程没有就会去执行任务
因此,当一个线程不满足某个运行条件地时候,我们可以让他进行等待,避免重复得到锁资源,导致其他线程处于线程饿死地状态。
notify方法:
notify 方法一般是结合wait方法使用的,他是用来唤醒被wait之后的线程。此外我们注意,在操作系统中notify并没有强制要求和锁一起使用,但是java中强制要求notify和锁一块使用。下面而我们通过代码以及问题来加深对这个的了解。
Demo1:
java
代码分析:
结果1:

结果二

我们发现一个代码可能出现两种情况,这是为什么呢?
首先我们知道线程是随机调度的,那么在线程启动之后 t1线程和t2 线程他们的执行顺序是不同的,我们观察结果不难发现,这两种不同的结果中t1和t2的执行顺序不一样,其中结果1 陷入堵塞的那一个是线程2 在执行。这其实是因为线程t2 先执行进行了notify导致,线程t1执行wait之后没有线程对他进行唤醒操作因此我们得到结论
结论1:wait要在notify之前,否则没办法进行唤醒
Demo2:
java
package Thread.WaitAndNotify;
public class Demo3 {
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(()->{
System.out.println("t1 wait之前");
synchronized (lock2){
try {
lock1.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t1 wait之后");
});
Thread t2 = new Thread(()->{
System.out.println("t2 notify之前");
synchronized (lock1){
lock1.notify();
}
System.out.println("t2 notify之后");
});
t1.start();
// t2.start();
}
}
代码分析:
在这里我们在synchorized的锁对象和wait的对象不同,我们发现代码抛出了监视器状态异常,这其实就是lock1释放锁的时候lock1对象并没有加锁
结论2: :synchorized的锁对象要和wait的一样 ,notify也同样如此
Demo3:
java
package Thread.WaitAndNotify;
public class Demo3 {
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(()->{
System.out.println("t1 wait之前");
synchronized (lock1){
try {
lock1.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t1 wait之后");
});
Thread t2 = new Thread(()->{
System.out.println("t2 notify之前");
synchronized (lock2){
lock2.notify();
}
System.out.println("t2 notify之后");
});
t1.start();
t2.start();
}
}
代码分析:
我们发现,我们对wait和notify采用了不同的对象t1线程又堵塞了,这是因为我们没有对lock2.notify()是对lock2对象唤醒,而lock1对象并没有被唤醒因此我们得出结论3
结论3:wait和notify要针对同一个对象
Demo4:
java
package Thread.WaitAndNotify;
public class Demo3 {
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(()->{
System.out.println("t1 wait之前");
synchronized (lock1){
try {
lock1.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t1 wait之后");
});
Thread t3 = new Thread(()->{
System.out.println("t3 wait之前");
synchronized (lock1){
try {
lock1.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t3 wait之后");
});
Thread t2 = new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t2 notify之前");
synchronized (lock1){
lock1.notify();
// lock1.notify();
}
System.out.println("t2 notify之后");
});
t1.start();
t3.start();
t2.start();
}
}
代码分析:
t1 和 t3 同时对lock1进行wait,t2对lock1进行唤醒。我们运行代码之后发现,t1和 t3 他们都有可能被唤醒,且唤醒是随机的,因此我们的结论
结论4:对于同一个对象有多个wait,一个notify只能唤醒一个,且随机唤醒
补充: notifyAll方法
对于一个对象有多个wait线程之后,java提供了一次唤醒全部的方法notifyAll方法,它可以一次唤醒所有该对象上的wait线程
总结:
- wait要在notify之前,否则代码即使notify了也会处于堵塞状态
- 对于一个对象由多个wait线程,一次notify只能唤醒一个线程,且随机唤醒
- 在notify的时候要加锁,同时也要注意锁是不是一一对应的
练习
练习1:顺序打印十次ABC
问题分析 :
打印十次ABC且使用多线程,分别打印A,B,C。那么我们就需要知道对于线程的调度是随机的,那么我们应该怎么得到顺序打印的ABC呢?也就是如何控制线程进行打印呢?
我们可以采用wait和notify方法进行控制,在线程A中,首先让线程A wait ,然后再线程A的最后唤醒B线程,在线程B中让线程B先wait,再在最后唤醒线程C,在线程C中首先让线程wait,然后让该线程唤醒A。这样就 构成了一个循环。当线程A被唤醒的时候,他就会唤醒线程B。而线程B也会唤醒线程C,线程C在唤醒线程A就这样构成了循环,就控制了打印。
代码:
java
package Thread.Work;
//顺序打印 ABC十次
// 打印在wait在for循环 否则就会导致 AAABBBCCC全都打印完才打印下一个
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
Object lock1 = new Object();
Object lock2 = new Object();
Object lock3 = new Object();
Thread t1 = new Thread(()->{
for(int i = 0;i<10;i++){
synchronized (lock1){
try {
lock1.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.print("A");
synchronized (lock2){
lock2.notify();
}
}
});
Thread t2 = new Thread(()->{
for(int i = 0;i<10;i++){
synchronized (lock2){
try {
lock2.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.print("B");
synchronized (lock3){
lock3.notify();
}
}
});
Thread t3 = new Thread(()->{
for(int i = 0;i<10;i++){
synchronized (lock3){
try {
lock3.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.print("C");
System.out.println();
synchronized (lock1){
lock1.notify();
}
}
});
t1.start();
t2.start();
t3.start();
Thread.sleep(500);
synchronized (lock1){
lock1.notify();
}
}
}
关键点:
- 在main线程里面 我们要先让main线程堵塞,这样ABC线程才能都堵塞,当我们对A进行唤醒的时候才能进入指定好的循环。
练习2,打印按照CBA打印线程的名字
问题分析:
我们还是使用wait和 notify让线程进入我们指定的循环当中,然后再main线程里面 notify一个,给他一点动力让他按照我们指定的顺序进行打印。
代码:
java
package Thread.Work;
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
Object lock1 = new Object();
Object lock2 = new Object();
Object lock3 = new Object();
Thread t1 = new Thread(()->{
synchronized (lock1){
try {
lock1.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName());
},"a");
Thread t3 = new Thread(()->{
synchronized (lock3){
try {
lock3.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName());
synchronized (lock2){
lock2.notify();
}
},"c");
Thread t2 = new Thread(()->{
synchronized (lock2){
try {
lock2.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName());
synchronized (lock1){
lock1.notify();
}
},"b");
t1.start();
t2.start();
t3.start();
Thread.sleep(500);
synchronized (lock3){
lock3.notify();
}
}
}
代码分析:
还是再main线程堵塞,让其他线程能够进入指定循环。
总结:
- wait方法,以及wait加锁特性,wait和join的区别,以及wait和sleep的区别,
- 线程饿死:线程一直得不到cpu资源导致线程无法进行工作
- notify方法,以及notify的注意事项
- wait和notify的练习加深理解
欢迎大佬留下三连!