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

【并发基础】线程的通知与等待:obj.wait()、obj.notify()、obj.notifyAll()详解

目录

〇、先总结一下这三个方法带来的Java线程状态变化

一、obj.wait()

1.1 作用

1.2 使用前需要持有线程共享对象的锁

1.3 使用技巧

二、obj.notify(All)()

1.1 notify() 方法

1.1.1 调用notify()或notifyAll()不会释放线程的锁

1.2 notifyAll() 方法

1.3 使用技巧

三、使用实例

四、wait()/notify()/notifyAll() 为什么定义在 Object 类中?


wait()、notify/notifyAll() 方法都是Object的本地final方法,无法被重写。

〇、先总结一下这三个方法带来的Java线程状态变化

当Java线程调用wait()方法后,该线程会进入等待队列,并且会释放占用的锁资源。线程状态会变为WAITING或TIMED_WAITING。该线程不会被挂起到外存,而是在内存中等待被唤醒。线程等待的条件通常是由其他线程调用notify()或notifyAll()方法来唤醒该线程。

当线程被唤醒时,它会重新尝试获取锁资源并从wait()方法返回。线程状态会变为BLOCKED,直到它获得了锁资源为止。如果成功获取锁资源,线程状态会变为RUNNABLE,然后可以继续执行。如果获取锁资源失败,则线程会继续等待,并且状态会维持在BLOCKED或WAITING或TIMED_WAITING状态,直到它再次被唤醒。

需要注意的是,线程在等待期间会消耗一定的资源,因此应该避免过多的线程等待。另外,线程在等待期间不会占用CPU时间片,因此可以减少CPU的利用率,提高系统的性能。

一、obj.wait()

1.1 作用

wait()是Object里面的方法,Object是所有对象的父类,即所有对象都可以调用wait()方法。wait方法还有可以传入等待时长的,可以让线程等待指定的时间后自动被唤醒。调用wait()会使Java线程进入到WAITING状态,调用wait(long time)会使Java线程进入到TIMED_WAITING状态(WAITING和TIMED_WAITING状态就是阻塞状态)

当一个线程调用一个共享变量的wait()方法时,该线程会阻塞(等待)。直到发生以下几种情况才会恢复执行:

  • 其他线程调用了该共享对象的 notify() 方法或者 notifyAll() 方法(继续往下走)
  • 其他线程调用了该线程的 interrupt() 方法,该线程会 InterruptedException 异常返回

等待线程:假设调用的是obj对象的wait()方法,wait的执行线程,也就是被暂停的线程,就称为对象obj上的等待线程。对象的wait方法可能被不同的线程执行,所以同一个对象可能会有多个等待线程。

1.2 使用前需要持有线程共享对象的锁

在使用wait()、notify()和notifyAll()方法方法前,需要先持有锁。如果调用线程共享对象的wait()、notify()和notifyAll()方法的线程没有事先获取该对象的监视器锁,调用线程会抛出IllegalMonitorStateException 异常。当线程调用wait() 之后,就会释放该对象的监视器锁

使用wait()notify()notifyAll()方法方法前,需要先持有锁:

  • 表象:wait、notify(ALL)方法需要调用 monitor 对象
  • 本质:Java的线程通信实质上是共享内存,而不是直接通信

那么,一个线程如何才能获取一个共享变量的监视器锁?

1、执行synchronized 同步代码块,使用该共享变量作为参数。

synchronized(共享变量) {// TODO
}

2、调用该共享变量的同步方法(synchronized 修饰)

synchronized void sum(int a, int b) {// TODO
}

 如下代码示例,线程A与线程B,在线程A中调用共享变量obj的wait()方法,在线程B中进行唤醒notify()。

/*** Object的Wati()方法的使用*/
@Slf4j
public class WaitTest {public static void main(String[] args) {// 定义一个共享变量Object obj = new Object();// 创建线程AThread threadA = new Thread(new Runnable() {@Overridepublic void run() {log.info("线程" + Thread.currentThread().getName()+"开始执行");try {// 获取共享变量的对象锁synchronized(obj){// 线程A 等待log.info("线程" + Thread.currentThread().getName()+"等待");// 调用wait(),线程A阻塞,并且释放掉获取到的obj的对象锁obj.wait();}} catch (InterruptedException e) {e.printStackTrace();}log.info("线程" + Thread.currentThread().getName()+"执行结束");}},"A");// 创建线程BThread threadB = new Thread(new Runnable() {@Overridepublic void run() {log.info("线程" + Thread.currentThread().getName()+"开始执行");// 获取共享变量锁synchronized (obj){//  线程B 唤醒或者中断  调用obj的唤醒操作或者使A线程中断的操作都可以将正在阻塞的A线程唤醒log.info("线程" + Thread.currentThread().getName()+"唤醒");obj.notify(); // 唤醒操作// threadA.interrupted(); // 中断操作}log.info("线程" + Thread.currentThread().getName()+"执行结束");}},"B");// 启动线程AthreadA.start();try {// 等待200ms,让线程B获取资源,在这200ms期间A就被阻塞了,释放了obj对象锁Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}// 启动线程BthreadB.start();}
}

执行结果:

线程A开始执行

线程B开始执行

线程B执行结束

线程A执行结束

可以看到主程序线程A启动之后,休眠了200ms让出cup执行权,线程B开始执行后调用notify()方法对阻塞线程A进行唤醒。

故:当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:(1)其他线程调用了该共享对象的notify()或者notifyAll()方法;(2)其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。

1.3 使用技巧

  • wait()方法一般配合while使用
    • 被唤醒后会重新竞争锁,之后从上次wait位置重新运行
    • while 多次判断,防止在wait这段时间内对象被修改

二、obj.notify(All)()

notify()和notifyAll()方法也是Object里面的方法,Object是所有对象的父类,即所有对象都可以调用notify()和notifyAll()方法。但是线程中共享变量在调用这两个方法前,该线程需要获取到这个共享变量的锁才可以,否则会抛出异常。

1.1 notify() 方法

一个线程调用共享对象的 notify() 方法后,会唤醒一个在该共享变量上调用 wait(...) 系列方法后阻塞的线程。

通知线程:调用notify/notifyAll方法时所在的线程叫做通知线程。

1.1.1 调用notify()notifyAll()不会释放线程的锁

当线程调用notify()或notifyAll()方法时,它不会释放掉线程持有的锁。

在Java中,每个对象都有一个相关联的锁,也称为监视器锁。当一个线程需要访问被该锁保护的对象时,它必须先获得该锁的所有权。所以只有获得锁的线程才能调用wait()、notify()和notifyAll()方法。

当线程调用notify()或notifyAll()方法时,它仅仅是唤醒等待在该对象上的一个或多个线程,以便它们可以继续执行。它不会释放线程持有的锁。因此,其他线程仍然无法访问被该锁保护的对象,直到调用notify()或notifyAll()方法的线程释放锁资源。

在多线程编程中,必须小心地管理锁,以避免死锁和竞争条件等问题。通常,为了确保线程安全和避免死锁,必须确保在访问共享资源时只有一个线程持有锁。当然,这也需要合理地使用wait()、notify()和notifyAll()方法来协调线程的执行顺序。

值得注意的是:

  • 一个共享变量上可能会有多个线程在等待,notify()具体唤醒哪个等待的线程是随机的
  • 被唤醒的线程不能马上从wait()方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,等到唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行

1.2 notifyAll() 方法

notifyAll() 方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。

1.3 使用技巧

尽量让notify / notifyAll()靠近临界区结束的地方。免得等待线程因为没有获得对象的锁,而又进入等待状态。

三、使用实例

比较经典的就是生产者和消费者的例子。

在生产者消费者模型中,推荐使用notifyAll,因为notify唤醒的线程不确定是生产者或消费者。

public class NotifyWaitDemo {// 共享变量队列的最大容量public static final int MAX_SIZE = 1024;// 共享变量public static Queue queue = new Queue();public static void main(String[] args) {// 生产者Thread producer = new Thread(() -> {// 获取共享变量的锁才能调用wait()方法synchronized (queue) {// 一般wait()都配合着while使用,因为线程唤醒后需要不断地轮循来尝试获取锁while (true) {// 当队列满了之后就挂起当前线程(生产者线程)// 并且,释放通过queue的监视器锁,让消费者对象获取到锁,执行消费逻辑if (queue.size() == MAX_SIZE) {try {// 阻塞生产者线程,并且使当前线程释放掉共享变量的锁queue.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 空闲则生成元素,并且通知消费线程queue.add();// 唤醒消费者来消费,建议用notifyAll(),因为notify()无法确定会唤醒哪一个线程queue.notifyAll();}}});// 消费者Thread consumer = new Thread(() -> {// 需要先获取锁synchronized (queue) {while (true) {// 当队列已经空了之后就挂起当前线程(消费者线程)// 并且,释放通过queue的监视器锁,让生产者对象获取到锁,执行生产逻辑if (queue.size() == 0) {try {// 阻塞消费者线程,并释放共享对象的锁queue.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 空闲则消费元素,并且通知生产线程queue.take();queue.notifyAll();}}});// 先执行生产者线程producer.start();try {// 将当前线程睡眠1000ms,让生产者先将队列生产满,然后wait阻塞起来,并且释放持有的锁。为了后续能执行消费者线程Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 执行消费者线程consumer.start();}// 共享变量static class Queue {private int size = 0;public int size() {return this.size;}// 生产操作public void add() {// TODOsize++;System.out.println("执行add 操作,current size: " +  size);}// 消费操作public void take() {// TODOsize--;System.out.println("执行take 操作,current size: " +  size);}}
}

 

四、wait()/notify()/notifyAll() 为什么定义在 Object 类中?

由于Thread类继承了Object类,所以Thread也可以调用者三个方法,等待和唤醒必须是同一个锁。而锁可以是任意对象,所以可以被任意对象调用的方法是定义在Object类中。


 相关文章:【并发基础】一篇文章带你彻底搞懂睡眠、阻塞、挂起、终止之间的区别
                  【并发基础】Java中线程的创建和运行以及相关源码分析           
                  【并发基础】线程,进程,协程的详细解释
                 【并发基础】操作系统中线程/进程的生命周期与状态流转以及Java线程的状态流转详解

http://www.lryc.cn/news/39011.html

相关文章:

  • css黏性定位-实现商城的分类滚动的标题吸附
  • @Component和@bean注解在容器中创建实例区别
  • 不写注释就是垃圾
  • 深信服一面
  • 【C语言】深度理解指针(中)
  • 步进电机运动八大算法
  • 如果你持续大量的教坏ChatGPT,它确实会变坏
  • opencv学习(二)图像阈值和平滑处理
  • 【含源码】用python做游戏有多简单好玩
  • C++常用函数
  • Android Framework基础到深入篇
  • 【Go进阶训练营】聊一下go的gc原理
  • 英飞凌Tricore原理及应用介绍05_中断处理之中断路由(IR)模块详解
  • 微搭问答002-移动端上传的文件如何在PC端下载
  • 初识JVM
  • 实践分享:Vue 项目如何迁移小程序
  • JavaScript学习笔记(6.0)
  • 某小公司面试记录
  • SPI读写SD卡速度有多快?
  • MySQL:索引与事物
  • mybatis实战
  • 【UEFI实战】BIOS与IPMI
  • 90%的人都不算会网络安全,这才是真正的白帽子技术【红队】
  • 关于vuex的使用
  • 第53篇-某商城sign参数分析-webpack【2023-03-07】
  • 探秘MySQL——排查与调优
  • 【9.数据页结构】
  • 演唱会总是抢不到票?教你用Python制作一个自动抢票脚本
  • 【系统开发】WebSocket + SpringBoot + Vue 搭建简易网页聊天室
  • Learning C++ No.14【STL No.4】