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

欢迎大佬留下三连!

相关推荐
Rysxt_29 分钟前
Spring Boot SPI 教程
java·数据库·sql
海边夕阳200629 分钟前
主流定时任务框架对比:Spring Task/Quartz/XXL-Job怎么选?
java·后端·spring·xxl-job·定时任务·job
Mr_万能胶35 分钟前
到底原研药,来瞧瞧 Google 官方《Android API 设计指南》
android·架构·android studio
q***985238 分钟前
VS Code 中如何运行Java SpringBoot的项目
java·开发语言·spring boot
帧栈1 小时前
开发避坑指南(72):HttpHeaders 的add()方法和set()方法有什么区别?
java·spring·http
unclecss1 小时前
把 Spring Boot 的启动时间从 3 秒打到 30 毫秒,内存砍掉 80%,让 Java 在 Serverless 时代横着走
java·jvm·spring boot·serverless·graalvm
tuokuac1 小时前
@PathVariable与@RequestParam
java·spring
q***16081 小时前
Tomcat的server.xml配置详解
xml·java·tomcat
程序员西西1 小时前
SpringBoot整合Apache Spark实现一个简单的数据分析功能
java·后端