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

多线程 - wait notify

回顾:

线程安全 --> 死锁,是在进行多线程的时候,比较常见的问题,也是比较严重的问题!!!

        1. 一个线程,一把锁。这个线程连续对这个锁加锁两次,如果这个锁不是可重入锁,就容易发生死锁现象。

        2. 两个线程,两把锁。线程 1 获取锁 A,线程 2 获取锁 B,之后,两个线程在不解开本来获取的锁的前提下,再彼此尝试获取对方的锁,就会发生死锁现象。

        3. N 个线程 M 把锁。哲学家就餐问题中是 N 个线程 N 把锁,如果是 N 个线程 M 把锁,场景变换一下,也会可能构造出死锁的。

死锁的必要条件:其中最重要的一点是:循环等待。 ==》 避免死锁的方案:约定加锁的顺序!

内存可见性问题:

一个线程读,一个线程写。在读的过程中,编译器可能会优化成读寄存器(这里也可能优化为读缓存了...),就导致写线程做出的修改,线程感知不到... ==》 使用 volatile 关键字来强制编译器提高准确度。

JMM 引入一些更加抽象的概念,重新表述了上述的过程,在 Java 规范文档中,表述更加严谨,通过引入主内存,工作内存这样的术语,表示了计算机实际的内存一起 CPU 寄存器 + CPU 缓存

wait 和 notify

wait 和 notify 翻译过来意思是 等待 和 通知 机制,和 join 用途类似,因为多个线程之间是 随机调度,抢占式调度的。

引入 wait 和 notify,就是为了能够从应用层面上,干预到多个不同线程代码的执行顺序,这里说的干预,不是影响系统的线程调度策略(系统内核中的线程调度,仍然是无序调度)

相当于是在应用程序代码中,让后执行的线程,主动放弃被调度的机会,就可以让执行的线程,先把对应的代码执行完了。

举一个栗子讲:

有 1 2 3 4 5 号大兄弟去 ATM 机中办业务,1 号大兄弟,长得人高马大的,兄弟们也就让他先进去了,当 1 号大兄弟进去取钱的时候,会有一种情况是 ATM 机里面没有钱,所以 1 号大兄弟就会先出来,等到有钱的时候再进去取,此时,其他大兄弟就会取竞争这个锁,争取进入到 ATM 中办理他们的业务,但是实际上,刚刚释放锁的 1 号大兄弟,他还会参与到锁的竞争中,此时,就完全有可能,是 1 号大兄弟,重新又拿到了锁,如此反复,此时就会导致一个情况:1 号大兄弟,反复获取锁,但是又无法完成实质性的逻辑,其他的大兄弟,又无法拿到锁,这个情况,称为“线程饿死”,也可以称为“线程饥饿”

线程饥饿这样的问题,属于是“概率性”的时间,不像死锁这样,一旦出现了之后,程序就一定会挂了,但是也会极大的影响其他线程的运行(占着茅坑不拉屎...)。

而且,像上述 ATM 的情况发生的概率,还是挺高的。

1 号大兄弟拿到了锁,处于 RUNNABLE 状态,其他线程因为锁冲突出现阻塞,BLOCKED 状态,近水楼台先得月,其他线程,需要系统唤醒之后,才能参与到锁竞争的,1 号大兄弟是不用被唤醒的,他直接就能竞争(当然这里到底是谁能竞争到,也是一个复杂的过程)。

这种情况当然也是 bug 了,虽然没有死锁那么严重,但是也是需要处理的...

这里的关键在于,1 号大兄弟,当他发现自己要执行的逻辑,前提条件不具备(ATM 里没钱),在这个情况下,1 号大兄弟,就应该主动的放弃对锁的争夺 / 主动放弃去 cpu 上调度执行(进入阻塞),一直等到,条件具备了(其他线程给 ATM 里存钱了),此时再接触阻塞,参与锁竞争。

此时,就可以使用到 wait 和 notify 了。

让 1 号大兄弟,发现当前他要执行的逻辑的前提条件不具备,则 wait,当其他线程让条件满足之后,再通过 notify 唤醒 1 号大兄弟...

 1. 线程之间的调度是无序的。

2. 1 号大兄弟,当前要完成的工作是想去 ATM 中取钱,进入 ATM 要加锁,但取钱的前提是 ATM 中里面是有钱的。如果有钱,就取走了,自然循环就 break 结束了,取钱动作就完了,如果没钱,就要再试依次,由于 1 号大兄弟也不知道什么时候有钱,就只能反复的进行尝试...

如果上面的代码中,ATM 是没钱的,此时 1 号大兄弟,就会反复快速的尝试加锁解锁,这个过程中,其他线程就大概率的很难再继续获取到锁了。

所以我们期望应该改成:

wait 的内部做了三件事:

        1. 释放锁

        2. 进入阻塞等待

        (1 2 两件事之后,其他线程就有机会拿到锁了)

        3. 当其他线程调用 notify 的时候,wait 就会解除阻塞,并重新获取到锁。

wait 和 join

join 是等待另一个线程执行完,当下线程才继续执行。

wait 则是等待另一个线程通过 notify 进行通知(不要求另一个线程一定执行完)

wait 的调用

和 synchronized 一样,随便一个对象,都可以进行 wait,即 wait 是 Object 中的方法

但如果,直接调用 wait,会出现异常:

逐渐分析异常信息: 不合法的 监视器(也就是锁)状态异常。

因为我们刚刚说了 wait 要做的三件事:1. 释放锁 2.进入阻塞等待 3. 当其他线程调用 notify 的时候,wait 就会接触阻塞,并重新获取到锁。

即,wait 一进来的时候,就要先释放锁,释放锁的前提是,wait 能先拿到锁。 ==》wait 必须要放到 synchronized 里面使用

每个对象里都有一把锁

 调用 wait 的对象,必须和 synchronized 中的锁对象是一致的!!!

因此,wait 解锁必然是解的 object 的锁,后续 wait 被唤醒之后,重新获取锁,当然还是获取到 object 的锁!!!在其他线程中调用 notify 的时候,也是需要使用同样的对象 --> object.notify()

wiat 演示:

如上图代码,如果仅仅在一个线程中调用 wait 而没有在其他线程中调用 notify 方法,则只会打印“wait 之前”,且代码就在 wait 这里阻塞了。

此时打开 jconsole 观察:

补充: wait 和 sleep join 都是一类的,都可能会被 interrupt 提前唤醒。

wait 的话,放到 synchronized 里面,是因为要释放锁,前提是先加上锁。

notify 其实可以不妨到 synchronized 中,不需要先加锁的(但是 Java 中特别约定要把 notify 放到 synchronized 里面了)(操作系统的原生 API 中也有 wait 和 notify 系统原生的 wait 需要先加锁,notify 不需要先加锁)

wait 和 notify 示例代码:

t1 线程启动,就会调用 wait,从而 t1 线程进入阻塞等待

t2 线程启动,就会先 sleep,sleep 时间到之后,再进行 notify 唤醒 t1

注意: t2 线程中的 sleep 要放在 synchronized 的外面,否则,由于随即调度,t1 t2 的运行顺序不确定,就有可能会 t2 先拿到锁,此时 t1 就没执行到 wait,t2 就 先 notify 了,结果就不符合预期了。(需要保证,代码是先执行 wait 后执行 notify)如果先 notify 虽然不会有副作用(不会有什么异常)但是 wait 就无法被唤醒,逻辑上出现问题。

上述代码的执行过程的理解:

        1. t1 执行起来之后,就会先立即拿到锁,并且打印“wait 之前”,ing进入 wait 方法(wait 方法会做两件事:释放锁 + 阻塞等待)

        2. t2 执行起来之后,先进性 sleep(5000) (这个 sleep 就可以让 t 能够先拿到锁)

        3. t2 sleep 结束之后,由于 t1 是wait 状态,锁是释放的,t2 就能拿到锁。接下来打印 t2 中的“notify 之前”,然后执行 notify 操作,这个操作就能够唤醒 t1 (此时 t1 就从 WAITING 状态恢复回来了)

        4. 但是由于 t2 此时还没有释放锁呢,t1 WAITING 恢复之后,尝试获取锁,就可能出现一个小小的阻塞,这个阻塞是由于锁竞争引起的(注意:肉眼很难看到 BLOCKED 状态,这个状态的变换是非常快的)

        5. t2 执行完 t2.notify 之后,释放锁,t2 执行完毕,t1 的 wait 也就可以重新获取到锁了,继续执行打印“wait 之后”。

补充:

        1. wait 提供了两个版本:

第一个版本中,wait 是“死等“,即,如果没有其他线程中调用 notify 方法的话,该线程就会一直等下去。第二个是带有超时间的等,单位是 ms,当进入 wait 的时候,最多等待 timeout ms,如果这个时间内,没有其他线程进行 notify 操作,那我们也不等了,继续进行。

死等这样的策略,我们一般是不会使用的...

        2. wait 和 notify 彼此之间是通过 object 对象联系起来的。

        object1.wait()  <==>  object2.notify() 此时是无法唤醒的!!!必须是两个对象一致才能唤醒!!!

        3. notifyAll ==》 唤醒这个对象上所有等待的线程。

假设有很多线程,都使用同一个对象 wait,针对这个对象进行 notifyAll,此时就会全部都唤醒。

但是注意,这些线程在 wait 返回的时候,要重新获取锁,就会因为锁的竞争,从而使得这些线程实际上是一个一个串行执行的(谁先拿到锁,谁后拿到锁,也是不确定的),相比之下,还是更倾向于使用 notify,notifyAll 全部唤醒线程之后,不太好控制。

        4. wait 和 sleep 的区别:

wait 提供了一个带有超时时间的版本,sleep 也能指定时间,都是时间到,就可以继续执行了,解除阻塞了。wait 和 sleep 都可以被提前唤醒(即时间没有到,也可以被唤醒),wait 通过 notify 唤醒, sleep 通过 interrupt 唤醒。

使用 wait 最主要的目标:一定是 不知道 要等多长时间的前提下使用,所谓超时时间,其实是”兜底的“。(大多数情况下,wait 都是在超时时间内就被唤醒了)

使用 sleep,一定是知道要等多长时间的前提下使用的,虽然能提前唤醒,但是通过异常唤醒,这个操作不应该作为”正确的业务流程“。

一般情况下,不使用 wait 来代替 sleep。

完!

相关文章:

  • Apache Commons Lang3 常用方法详解
  • 大数据(4.3)Hive基础查询完全指南:从SELECT到复杂查询的10大核心技巧
  • 【超分辨率】基于DDIM+SwinUnet实现超分辨率
  • 深入理解pthread多线程编程:从基础到生产者-消费者模型
  • Android: Handler 的用法详解
  • 【工具】在 Visual Studio 中使用 Dotfuscator 对“C# 类库(DLL)或应用程序(EXE)”进行混淆
  • 关于 Nginx 配置中 proxy_set_header Host $host 的作用及其对 HTTP 请求头影响的详细说明,结合示例展示设置前后的差异
  • 【VSCode SSH 连接远程服务器】:身份验证时,出现 key: invalid format 的问题
  • 服务端向客户端推送数据的实现方案
  • Linux | I.MX6ULL 终结者底板原理图讲解完(第六天)
  • 关于亚马逊TTS的笔记
  • 银行回单识别技术应用与API服务解析
  • 1 分钟掌握 PlantUML,快速绘制 UML 类图!
  • Docker学习--本地镜像管理相关命令--docker history 命令
  • 在Windows下使用Docker部署Nacos注册中心(基于MySQL容器)
  • 初识C++(入门)
  • kubernetes》》k8s》》Deployment》》ClusterIP、LoadBalancer、Ingress 内部访问、外边访问
  • 31天Python入门——第20天:魔法方法详解
  • TruPlasma RF 1002-G2/13 软件 TruPlasma RF 1003-G2/13软件 TRUMPF 调试监控软件
  • SQL Server:用户权限
  • 哪条线路客流最大?哪个站点早高峰人最多?上海地铁一季度客流报告出炉
  • 外企聊营商|上海仲裁:化解跨国企业纠纷的“上海路径”
  • 埃尔多安:愿在土耳其促成俄乌领导人会晤
  • 浙江省台州市政协原副主席林虹被“双开”
  • 国新办将就2025年4月份国民经济运行情况举行新闻发布会
  • 王征、解宁元、牛恺任西安市副市长