<JAVAEE> 多线程4-wait和notify方法

引言

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的练习加深理解

欢迎大佬留下三连!

相关推荐
华仔啊1 小时前
Stream 代码越写越难看?JDFrame 让 Java 逻辑回归优雅
java·后端
ray_liang1 小时前
用六边形架构与整洁架构对比是伪命题?
java·架构
Ray Liang3 小时前
用六边形架构与整洁架构对比是伪命题?
java·python·c#·架构设计
Java水解3 小时前
Java 中间件:Dubbo 服务降级(Mock 机制)
java·后端
砖厂小工5 小时前
用 GLM + OpenClaw 打造你的 AI PR Review Agent — 让龙虾帮你审代码
android·github
张拭心5 小时前
春节后,有些公司明确要求 AI 经验了
android·前端·人工智能
张拭心6 小时前
Android 17 来了!新特性介绍与适配建议
android·前端
SimonKing7 小时前
OpenCode AI辅助编程,不一样的编程思路,不写一行代码
java·后端·程序员
FastBean7 小时前
Jackson View Extension Spring Boot Starter
java·后端
Kapaseker8 小时前
Compose 进阶—巧用 GraphicsLayer
android·kotlin