Java多线程基础-7:wait() 和 notify() 用法解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 这篇内容探讨了Java中的`wait()`和`notify()`方法在多线程编程中的使用。

一、为什么需要 wait() 和 notify() ?


举一个典型的例子:







这里,ATM机就看作是锁,而4位老哥就是竞争锁的4个线程。1号老哥在ATM机进进出出,并没有实质性地释放锁;但由于ATM机中始终没钱,1号老哥也取不了钱。1号老哥陷入忙等,其它老哥竞争不到ATM机(即竞争不到CPU资源),一直在阻塞,也什么事情都干不了。


这里就出现了一个问题:线程饿死。 有些同学可能疑惑,线程不是会有记账信息吗?记账信息不就可以解决问题吗?但实际上,线程的记账信息是一个比较宏观的东西,它需要多个线程多运行一段时间才能生成,而上面线程饿死时1号线程进进出出的情况是一瞬间的事情,因此记账信息也无法解决。


而使用 wait() 和 notify() 就可以有效解决上述问题。如果情况是这样的:




wait():发现条件不满足或时机不成熟时,就先让1号线程先阻塞等待。


notify(): 当其它线程构造了一个成熟的条件时,就可以通知唤醒1号。1号被唤醒后,又可以参与锁的竞争了。


这也类似于篮球场上运动员的活动。每个运动员都是独立的 “执行流”,可以认为是一个 “线程”。而完成一个具体的进攻得分动作,则需要多个运动员相互配合,按照一定的顺序执行一定的动作:线 程1先 “传球”, 线程2才能 “扣篮”。要扣篮的运动员需要wait传球的运动员把球传过去,才能扣篮;而传球的运动员把球传过去这一操作就类似于notify;扣篮运动员拿到了球,就结束了wait,可以进行后续的操作即扣篮了。


二、方法用法解析


1、wait() 与 notify()


(1)wait() 的作用、结束等待的条件与使用条件


wait 做的事情:


  • 释放当前的锁。


  • 使当前执行代码的线程进入阻塞等待(把线程放到等待队列中)。

满足一定条件时(收到通知时)被唤醒,同时重新尝试获取这个锁。


wait 结束等待的条件:


  1. 其他线程调用该对象的 notify 方法。


  1. wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本,来指定等待时间)。


  1. 其他线程调用该等待线程的 interrupted 方法,导致 wait 抛出 InterruptedException 异常。


wait() 方法的官方文档解释如下:  




注意wait()的使用条件:wait() 必须搭配 synchronized 来使用,wait()必须写到synchronized代码块里面(notify 方法也必须在synchronized代码块中使用)。脱离 synchronized 使用 wait() 会直接抛出异常。如以下代码所示:



public class Test {
    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        System.out.println("wait之前:");
        obj.wait(); // 所有可能会引起线程阻塞等到的方法都会抛出 InterruptedException 受查异常
        System.out.println("wait之后:");
    }
}


这个代码中,直接调用了obj.wait()而没有加锁,运行后,抛出了 IllegalMonitorStateException(非法的锁状态异常):




如果还没有获取到锁就尝试解锁,运行后就会抛出非法的锁状态异常。这里的代码抛出该异常正是这个原因:wait()方法内部有一步重要的操作:先解锁,再阻塞等待。


因此,在使用 wait() 前,必须先加锁,把wait()写到synchronized代码块内部。同时,Java也规定调用 notify() 也必须在synchronized代码块中。


并且,加锁的锁对象必须要与调用wait()的锁对象是同一个。如果加锁对象与调用wait()的对象不是同一个,也会抛出 IllegalMonitorStateException 异常。


synchronized (obj) {
    obj.wait(); 
}


(2)wait() 与 notify() 的使用示例


注意:在代码中使用wait()和notify()时,必须先执行wait()再执行notify()才有效。如果还没有wait(),就调用了notify(),此时就相当于一炮打空了。此时wait()的线程无法被唤醒,但代码也不会抛出其他异常(没有额外的副作用,但代码也不能正确执行)。


运行以下代码:分别创建了t1和t2两个线程,它们对同一个对象加锁,并且让t1线程中执行wait(),t2线程中执行notify()。先后启动t1和t2线程,观察结果:



public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
 
        Thread t1 = new Thread(() -> {
                try {
                    System.out.println("wait开始");
                    synchronized (locker) {
                        locker.wait();
                    }
                    System.out.println("wait结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        });
 
        t1.start();
 
        Thread.sleep(1000); //保证t1先启动,wait()先执行
 
        Thread t2 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("notify开始");
                locker.notify();
                System.out.println("notify结束");
            }
        });
 
        t2.start();
    }
}


运行结果:





3)分析代码运行结果

在上述代码中,t1先执行。当t1执行到wait()时候,t1的锁被wait()释放,且t1自身进入阻塞等待状态。

1s后,t2获取到锁并开始执行。当t2执行到notify()时,就会通知t1线程唤醒。

注意,由于notify()是在synchronized的内部,因此只有等到t2释放了锁,t1才能再竞争到锁,并继续向下执行。因此,控制台上先输出“notify结束”,再输出“wait结束”。

在上述代码中,虽然t1是先执行的,但是可以通过wait(),notify()的控制让t2先执行一些逻辑;t2执行完之后,notify()唤醒t1(相当于传球操作),t1再继续向下执行(相当于扣篮操作)。这就体现了所谓“wait和notify让线程执行的顺序更加可控”。


而wait()的初心,正是为了实现阻塞的效果。只有某一个线程暂时进入阻塞了,线程之间的执行顺序才能发生变化。


并且,wait()阻塞等待会让线程进入 WAITING 状态。


(4)join() 与 wait() 的区别

这里也体现了 join() 与 wait() 作用效果上的一个区别。


join() 只能是让t2线程先执行完,再继续执行t1,此时t1与t2之间一定是串行的。而通过wait()和notify(),可以让t2执行完一部分,再让t1执行……t1执行一部分,再让t2执行……t2再执行一部分,再让t1执行……


(5)带参数的wait方法:wait(timeout)

wait() 方法存在一种重载的可以带参数方法。带参数的方法 wait(long timeout) 可以指定一个等待的超时时间timeout。如果线程的wait的时长大于等于超时时间timeout后还没有别的线程notify它,它就会自己唤醒自己。




(6)notifyAll() 唤醒等待同一对象的全部线程

唤醒操作还有一个notifyAll()。


可能有一种情况:有多个线程,wait着同一个对象。比如,在t1、t2、t3中都调用了object.wait(),此时如果在main中调用的是object.notify(),那么会随机唤醒上述的一个线程,而另外两个线程仍然处于waiting状态;如果是调用了object.notifyAll(),此时就会把上述三个线程都唤醒,然后这三个线程都会重新竞争锁,然后依次执行。


注意,仍然要等到t1、t2、t3的都wait()了再去notify()它们,不然又要“空打一炮”了。


三、wait() 与 sleep()、join() 的区别


1、wait() 和 sleep()


这两个方法最大的区别在于被设计出来的初心(即要解决的问题)不同。wait() 要解决的是线程之间的顺序控制问题,而sleep()只是单纯地让当前线程休眠一会儿。


正因如此,进一步地在使用上也有明显的区别,如wait()必须搭配锁来使用,而sleep()不需要。sleep() 是让程序暂停执行指定的时间并让出CPU给其它线程,当时间到了又会自动恢复运行状态;而wait()只有被唤醒之后,线程才会重新尝试获取锁,获取到了锁才能继续执行。


2、wait() 和join()


join()方法是让线程t1等待线程t2线程完全结束(完成其执行)再执行。它主要起同步的作用,使线程之间的执行从“并行”变成“串行”。也就是说,当我们在线程t1中调用了线程t2的join()方法时,线程执行过程发生改变:线程t1必须等待线程t2执行完毕后,才可以继续执行下去。


二者存在于不同的java包中(wait()方法在java.lang.Object中声明,而join()方法在java.lang.Thread中声明),wait()方法用于线程间通信(notify() 唤醒正在wait()的线程,这样的交互看成是一种通信),而join()方法用于在多个线程之间添加排序:第二个线程需要在第一个线程完全执行完成后才能开始执行。


此外,我们可以通过使用notify()或notifyAll()方法唤醒wait(),但是我们不能中途打破join()所施加的等待。


最后,wait()需要在synchronized代码块中使用,而join()不需要。


四、总结


1、wait() 和 notify() 方法都必须搭配 synchronized 和同一个锁对象,如果wait()和notify()作用于不同的锁对象,是没有任何作用的。


2、如果一个线程调用对象的notify()方法,但该线程并不处于wait的状态中,notify()不会产生作用(也没有副作用)。


3、如果有多个线程在wait(),notify()是只随机唤醒一个,而notifyAll()则是唤醒所有。


相关文章
|
5天前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
11天前
|
JSON Shell Linux
dockerfile 用法全解析
Dockerfile指令简介:`FROM`基于Alpine镜像;`WORKDIR`设置工作目录;`COPY`复制文件;`ADD`支持URL;`RUN`运行命令;`CMD`容器启动时执行;`ENTRYPOINT`与`CMD`组合执行;`EXPOSE`声明端口;`VOLUME`映射文件;`ENV`设置环境变量;`ARG`构建参数;`LABEL`元数据;`ONBUILD`触发命令;`STOPSIGNAL`停止信号;`HEALTHCHECK`健康检查;`SHELL`默认Shell。Alpine仅5M,小巧高效。
42 4
dockerfile 用法全解析
|
3天前
|
Java 数据库连接 Spring
反射-----浅解析(Java)
在java中,我们可以通过反射机制,知道任何一个类的成员变量(成员属性)和成员方法,也可以堆任何一个对象,调用这个对象的任何属性和方法,更进一步我们还可以修改部分信息和。
|
27天前
|
Java 编译器
Java 泛型详细解析
本文将带你详细解析 Java 泛型,了解泛型的原理、常见的使用方法以及泛型的局限性,让你对泛型有更深入的了解。
42 2
Java 泛型详细解析
|
28天前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
55 12
|
25天前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
3天前
|
Java 调度
|
25天前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
27天前
|
Java 数据库连接 开发者
Java中的异常处理机制:深入解析与最佳实践####
本文旨在为Java开发者提供一份关于异常处理机制的全面指南,从基础概念到高级技巧,涵盖try-catch结构、自定义异常、异常链分析以及最佳实践策略。不同于传统的摘要概述,本文将以一个实际项目案例为线索,逐步揭示如何高效地管理运行时错误,提升代码的健壮性和可维护性。通过对比常见误区与优化方案,读者将获得编写更加健壮Java应用程序的实用知识。 --- ####
|
1月前
|
存储 缓存 监控
Java中的线程池深度解析####
本文深入探讨了Java并发编程中的核心组件——线程池,从其基本概念、工作原理、核心参数解析到应用场景与最佳实践,全方位剖析了线程池在提升应用性能、资源管理和任务调度方面的重要作用。通过实例演示和性能对比,揭示合理配置线程池对于构建高效Java应用的关键意义。 ####

推荐镜像

更多