当前位置: 首页 > news >正文

Java 中 wait 与 notify 的详解:线程协作的关键机制

wait 与 join 的核心区别

这两个关键字都是用来协调线程之间执行顺序的工具,但有着本质区别:

  • join:等待另一个线程彻底执行完毕后,当前线程才继续往下执行。
  • wait:等待另一个线程执行notify/notifyAll后,当前线程就继续往下执行(不需要等待另一个线程完全执行完毕)

场景引入

假设现在有多个“线程”去ATM中“取钱”。(这就模拟了多个线程竞争同一把锁的场景
排在最前面的“线程”进入ATM之后,在它出来之前,后面的线程就无法进入。
假设当第一个线程进入之后,发现此时的ATM中并没有钱,那么它就会从ATM中出来。
那么问题来了:当第一个线程出来之后,第二个线程会直接进入ATM中吗
答案是不一定。
这里我们要明确的一点是,当多个线程竞争同一把锁时,当获取到锁的线程释放之后,其他哪个线程拿到这把锁是随机的,这是由于操作系统的调度是随机的。
此时要注意的是:

  1. 当前释放这个锁的线程是就绪状态
  2. 其他线程都属于在这个锁上进行阻塞等待,是阻塞状态

所以当前这个线程是有很大概率继续拿到这把锁的。
如果这个线程一直这样进行“反复横跳”,那么这就会导致其他线程一直不可以在CPU中执行,
这就是所谓的“线程饿死”现象。

上述场景就是wait和notify应用的典型场景
我们可以这样进行优化:当拿到锁的线程发现要执行的任务的时机尚未成熟的时候,就可以使用wait进行阻塞等待,在等待的这段时间里,其他的线程就有机会到CPU上去执行。
这个优化就可以节省许多不必要的开销。
以上述场景为例,当第一个线程发现ATM中并没有钱的时候,它就可以进行wait阻塞等待,当ATM中有钱之后,我们再使用notify关键字把它唤醒。

下面我们结合代码来详细讲解这两个关键字

代码解析

public static void main(String[] args) throws InterruptedException {Object object=new Object();System.out.println("wait之前");object.wait();System.out.println("wait之后");}

上述代码运行之后会报错,其中的IllegalMonitorStateException意为“非法的锁状态”。
这是为什么呢?
这是因为object.wait()这行代码在执行的第一步就是先释放object对象所对应的锁,那么能够释放锁的前提是object对象应该处于加锁状态,这样才可以释放。
下面我们给它加一个锁就可以让程序正常运行,示例如下:

  Object object=new Object();System.out.println("wait之前");synchronized (object){object.wait();}System.out.println("wait之后");

下面我们再来详细解析一下这些代码:

 synchronized (object){object.wait();}
  • 当运行到第一个大括号之后一直到object.wait();这行代码之前一直是加锁状态;
  • 当运行到这行代码时(wait在等待的过程中),是解锁状态(尽管这个时候并没有运行到右大括号);
  • 当运行完这行代码之后又是加锁状态;
  • 运行到右大括号就是解锁状态。

这里还要注意的一个细节是synchronized的锁对象和wait的锁对象必须是同一个~~~

wait和notify结合使用

 public static void main(String[] args) {Object locker1=new Object();Object locker2=new Object();Thread t1=new Thread(()->{try {System.out.println("wait之前");synchronized (locker1){locker1.wait();}System.out.println("wait之后");} catch (InterruptedException e) {e.printStackTrace();}});Thread t2=new Thread(()->{Scanner scanner=new Scanner(System.in);System.out.println("请输入任意内容唤醒t1");scanner.next();synchronized (locker1){locker1.notify();}});t1.start();t2.start();}


这里的locker1.notify();同样需要先拿到锁,再进行操作(这是Java中给出的限制)
wait是必须要搭配锁来使用的,因为wait需要先释放锁;
而notify操作在原则上说并不涉及到加锁解锁操作。
我们要知道的是,线程和锁本身就是操作系统所支持的特性。
在操作系统原生的API中,wait必须搭配锁来使用而notify则不需要。
我们这里给notify与锁搭配使用,是为了遵循Java给出的限制

  synchronized (locker1){locker1.wait();}synchronized (locker1){locker1.notify();}

这里要注意的是,这四处的锁对象必须保持一致。
如果这些锁对象是不同的,那么这两个线程之间就无法建立联系。

上面两个线程的先后顺序是随机的,但是我们要保证的是wait一定要在notify之前执行
如果先执行notify再执行wait,那么wait将无法被唤醒。

多个线程在同一个对象上进行wait,此时再用notify唤醒

public static void main(String[] args) {Object locker1=new Object();Thread t1=new Thread(()->{System.out.println("wait之前");synchronized (locker1){try {locker1.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t1wait之后");});Thread t2=new Thread(()->{System.out.println("wait之前");synchronized (locker1){try {locker1.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t2wait之后");});Thread t3=new Thread(()->{Scanner scanner=new Scanner(System.in);System.out.println("请输入任意内容唤醒线程");scanner.next();synchronized (locker1){locker1.notify();}});t1.start();t2.start();t3.start();}


这次唤醒的是t1,经过多次运行后我们发现,当只进行一次唤醒操作的时候,所唤醒的线程是随机的。
要想唤醒全部线程,要么就多执行几次locker1.notify()操作,要么就是用notifyAll()方法。

运行结果分析:

  • 当多个线程在同一个对象上等待时,一次 notify 操作只会随机唤醒其中一个线程
  • 要唤醒全部线程,可以多次调用notify(),或直接使用notifyAll()方法

虽然使用 notifyAll () 方法可以一次唤醒所有线程,但被唤醒的线程需要重新竞争锁 —— 只有一个线程能成功获取锁并继续执行。
其他线程会因竞争锁失败再次进入阻塞状态,直至持有锁的线程释放锁后重新参与竞争。

总结

wait 与 notify 是 Java 中实现线程间协作的重要机制,它们与锁紧密配合,能够有效解决线程竞争导致的问题(如线程饿死)。
正确理解和使用这两个方法,对于编写高效、可靠的多线程程序至关重要。使用时需特别注意锁对象的一致性和 wait/notify 的调用顺序,以避免程序出现难以调试的并发问题。


文章转载自:

http://NHxKN9FL.qdLnw.cn
http://hb0glv9q.qdLnw.cn
http://J7XruId6.qdLnw.cn
http://7x74vPEc.qdLnw.cn
http://RN1rAVSZ.qdLnw.cn
http://WLaD9CN3.qdLnw.cn
http://SGc1kuEm.qdLnw.cn
http://fIChRQZO.qdLnw.cn
http://q7pJwusm.qdLnw.cn
http://qpIjCqoC.qdLnw.cn
http://0BLYbEGu.qdLnw.cn
http://bCZGgI29.qdLnw.cn
http://7slFKHUm.qdLnw.cn
http://FMJ3itUs.qdLnw.cn
http://uSf9WCdT.qdLnw.cn
http://Stdq7wjO.qdLnw.cn
http://hz16pUXD.qdLnw.cn
http://a8g2EKW7.qdLnw.cn
http://vhkJYxb7.qdLnw.cn
http://KCiE9I6W.qdLnw.cn
http://hAAnljem.qdLnw.cn
http://OZSD5NMm.qdLnw.cn
http://dsjjwZlk.qdLnw.cn
http://gQJSl2TT.qdLnw.cn
http://TFJdpxKz.qdLnw.cn
http://Dd7U46zl.qdLnw.cn
http://3FGgJqit.qdLnw.cn
http://WwTYDriX.qdLnw.cn
http://98FkpeIX.qdLnw.cn
http://luBj5UeM.qdLnw.cn
http://www.dtcms.com/a/373935.html

相关文章:

  • Linux下编译Gmsh
  • api-ms-win-crt-runtime-l1-0.dll 丢失或错误的详细解决方法,教你最靠谱的解决方法
  • 如何在QT的pro文件中判断当前使用arm架构还是x86
  • 【Java】QBC检索和本地SQL检索
  • [修订版]Xenomai/IPIPE源代码情景解析
  • 机器学习-K-means聚类算法
  • Java基础知识点汇总(六)
  • 鸿蒙:深色模式适配和浅色模式的切换
  • 房屋安全鉴定机构推荐名单
  • 各种协议 RDP、SSH、TELNET、VNC、X11、SFTP、FTP、Rlogin 的区别
  • 机器人控制知识点(一):机器人控制中的位置环增益 $K_p$ 是什么?
  • 米勒平台开通和关断过程分析
  • 【ComfyUI】混元3D 2.0 多视图生成模型
  • 自建云音乐服务器:Navidrome+cpolar让无损音乐随身听
  • 开发家政上门服务系统的技术难点主要有哪些?
  • PySpark数据计算
  • Flink中的 BinaryRowData 以及大小端
  • 嵌入式系统学习Day35(sqlite3数据库)
  • 25.9.8 C++day8作业
  • PySpark数据输入
  • C++工程实战入门笔记13-多态
  • Python元组:不可变但灵活的数据容器
  • 设计模式(策略,观察者,单例,工厂方法)
  • C++智能指针(先行版)
  • 安卓蓝牙文件传输完整指南
  • C++读文件(大学考试难度)
  • 拆解LinuxI2C驱动之mpu6050
  • Linux--线程
  • 中大型水闸安全监测的关键环节与措施
  • 基于QMkae/CMake配置QT生成的exe图标