Java 并发编程实战
王宝令
资深架构师
72485 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 51 讲
学习攻略 (1讲)
Java 并发编程实战
15
15
1.0x
00:00/00:00
登录|注册

06 | 用“等待-通知”机制优化循环等待

notifyAll()
notify()
wait()
细节
完善的等待-通知机制
尽量使用notifyAll()
小试牛刀:一个更好地资源分配器
用synchronized实现等待-通知机制
现实世界的就医流程
Java语言支持
课后思考
总结
等待-通知机制
死循环方式循环等待
优化循环等待

该思维导图由 AI 生成,仅供参考

由上一篇文章你应该已经知道,在破坏占用且等待条件的时候,如果转出账本和转入账本不满足同时在文件架上这个条件,就用死循环的方式来循环等待,核心代码如下:
// 一次性申请转出账户和转入账户,直到成功
while(!actr.apply(this, target))
如果 apply() 操作耗时非常短,而且并发冲突量也不大时,这个方案还挺不错的,因为这种场景下,循环上几次或者几十次就能一次性获取转出账户和转入账户了。但是如果 apply() 操作耗时长,或者并发冲突量大的时候,循环等待这种方案就不适用了,因为在这种场景下,可能要循环上万次才能获取到锁,太消耗 CPU 了。
其实在这种场景下,最好的方案应该是:如果线程要求的条件(转出账本和转入账本同在文件架上)不满足,则线程阻塞自己,进入等待状态;当线程要求的条件(转出账本和转入账本同在文件架上)满足后,通知等待的线程重新执行。其中,使用线程阻塞的方式就能避免循环等待消耗 CPU 的问题。
那 Java 语言是否支持这种等待 - 通知机制呢?答案是:一定支持(毕竟占据排行榜第一那么久)。下面我们就来看看 Java 语言是如何支持等待 - 通知机制的。

完美的就医流程

在介绍 Java 语言如何支持等待 - 通知机制之前,我们先看一个现实世界里面的就医流程,因为它有着完善的等待 - 通知机制,所以对比就医流程,我们就能更好地理解和应用并发编程中的等待 - 通知机制。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

通过“等待-通知”机制优化循环等待 本文介绍了如何通过“等待-通知”机制来优化循环等待的问题。在并发编程中,当线程要求的条件不满足时,传统的循环等待方式可能会导致CPU消耗过大。文章提出了使用Java语言内置的synchronized配合wait()、notify()、notifyAll()方法来实现等待-通知机制的方案。通过对比现实世界的就医流程,阐述了等待-通知机制的重要性和复杂性。在具体实现方面,通过使用synchronized来实现互斥锁,以及wait()、notify()、notifyAll()方法来实现线程的等待和通知。最后,通过一个资源分配器的示例代码展示了如何利用“等待-通知”机制来解决一次性申请资源的问题。这种机制能够避免循环等待消耗CPU,并且能够更好地保证线程的效率和资源的分配。文章通过清晰的示例和图示,帮助读者理解了“等待-通知”机制的基本原理和实现方式,为并发编程中的优化提供了有益的参考。 文章还提到了使用notify()和notifyAll()的区别,以及wait()方法和sleep()方法的区别,为读者提供了更深入的思考和理解。通过本文的总结,读者可以快速了解并发编程中“等待-通知”机制的重要性,以及如何利用Java语言提供的方法来实现这一机制。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 并发编程实战》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(218)

  • 最新
  • 精选
  • crazypokerk
    wait()方法与sleep()方法的不同之处在于,wait()方法会释放对象的“锁标志”。当调用某一对象的wait()方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直到调用了notify()方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。当调用了某个对象的notifyAll()方法,会将对象等待池中的所有线程都移动到该对象的锁标志等待池。 sleep()方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是sleep()方法不会释放“锁标志”,也就是说如果有synchronized同步块,其他线程仍然不能访问共享数据。

    作者回复: 👍

    2019-03-12
    8
    252
  • wang
    public class MyLock { // 测试转账的main方法 public static void main(String[] args) throws InterruptedException { Account src = new Account(10000); Account target = new Account(10000); CountDownLatch countDownLatch = new CountDownLatch(9999); for (int i = 0; i < 9999; i++) { new Thread(()->{ src.transactionToTarget(1,target); countDownLatch.countDown(); }).start(); } countDownLatch.await(); System.out.println("src="+src.getBanalce() ); System.out.println("target="+target.getBanalce() ); } static class Account{ //账户类 public Account(Integer banalce) { this.banalce = banalce; } private Integer banalce; public void transactionToTarget(Integer money,Account target){//转账方法 Allocator.getInstance().apply(this,target); this.banalce -= money; target.setBanalce(target.getBanalce()+money); Allocator.getInstance().release(this,target); } public Integer getBanalce() { return banalce; } public void setBanalce(Integer banalce) { this.banalce = banalce; } } static class Allocator { //单例锁类 private Allocator(){} private List<Account> locks = new ArrayList<>(); public synchronized void apply(Account src,Account tag){ while (locks.contains(src)||locks.contains(tag)) { try { this.wait(); } catch (InterruptedException e) { } } locks.add(src); locks.add(tag); } public synchronized void release(Account src,Account tag){ locks.remove(src); locks.remove(tag); this.notifyAll(); } public static Allocator getInstance(){ return AllocatorSingle.install; } static class AllocatorSingle{ public static Allocator install = new Allocator(); } } }

    作者回复: 高手高手,让我写也这不这样好👍👍👍👍👍

    2019-03-12
    20
    127
  • 虎虎❤️
    困惑 1. 对于从来没有获得过互斥锁的线程 所在的等待队列 和 因为wait() 释放锁而进入了等待队列,是否是同一个等待队列?也就是图中左侧和右侧的是否为同一个队列? 2. notifyAll() 会发通知给等待队列中所有的线程吗?包括那些从未获得过互斥锁的线程吗? 3. 因为wait()被阻塞,而又因为notify()重新被唤醒后,代码是接着在wait()之后执行,还是重新执行 apply 方法?

    作者回复: 不是一个队列 只唤醒右侧的队列 wait之后

    2019-03-14
    18
    107
  • 我是卖报小行家
    wait和sleep区别 1:wait释放资源,sleep不释放资源 2:wait需要被唤醒,sleep不需要 3:wait需要获取到监视器,否则抛异常,sleep不需要 4:wait是object顶级父类的方法,sleep则是Thread的方法

    作者回复: 全面👍

    2019-03-12
    61
  • kaizhi
    感觉老师讲解的节奏非常好,能把并发讲解的这么浅显易懂绝非一日之功。老师在用一种由浅入深,逐层深入的方法来讲解java并发的这些知识点,而不是一股脑的把所有的知识点都罗列出来,有助于我们的吸收,也能引发我们的进一步思考,譬如这节的wait方法,就是在改进上一节的while循环等待(上一节的while在评论区就引发了各路高手的强烈不满,哈哈),这样有助于我们理解当年java的开发者在设计wait方法时的出发点在哪里,另外也让我们理解了为什么wait,notify,notifyAll这些方法会作为Object类的方法。用现实生活做类比这一点也很赞,之前有艺术来源于生活而又高于生活,现在可以说技术来源于生活而又高于生活了,哈哈~

    作者回复: 感谢你这么懂我😂

    2019-03-15
    2
    60
  • 单小灰
    老师notify和notifyAll那块的区别,我相信有很多人和我一样有疑惑,在经过细想之后我终于搞明白了,这里简单举个例子来说明老师的意思:有两个顾客要买水果,但同时只能有一个人进店里买(也就是只有有抢到锁的人才能进去买水果),顾客A想要买橘子,顾客B想要买苹果,但是目前店里什么都没有,那么A和B都在while循环里面调wait方法进行阻塞等待(这时候锁已经释放),然后店员C去进货进了苹果,然后开始通知大家可以来买水果了(也就是调用锁的notify方法),这里notify方法随机唤醒一个顾客,假设唤醒了顾客B,顾客B拿到锁之后发现要的橘子还是没有(对应while循环的条件还是没满足)又调了wait进行阻塞等待,结果这样就导致明明有苹果,但是A还是等在死那。但如果是notifyAll方法的话,那么就同时通知A和B(唤醒A和B),这时两个顾客竞争锁,假设拿到锁的还是B,他发现没有橘子于是接着wait释放锁,这时候A就能拿到B释放的锁,然后就可以买到想要的苹果了,这样就不会出现上面发生的死等现象。

    作者回复: 我跟你说,你简直太有才了!这是我见过的最清晰的描述!

    2020-09-29
    6
    45
  • aksonic
    老师,我昨天问了你问题后,带着疑问又去学习了下,是不是文章中的左边和右边的两个队列应该改一改名字,不应该都叫等待队列,这样对新手很容易产生误解。如果左边的叫做同步队列,右边的叫做等待队列可能更好。左边的队列是用来争夺锁的,右边的队列是等待队列,是必须被notify的,当被notify之后,就会被放入左边的队列去争夺锁。老师,你觉得呢?

    作者回复: 你这个建议挺好,在管程里面,会重新讲这俩队列。现在就知道有俩等待队列就可以了

    2019-03-14
    5
    31
  • 老杨同志
    点赞@姜戈 补充一下:wait与sleep区别在于: 1. wait会释放所有锁而sleep不会释放锁资源. 2. wait只能在同步方法和同步块中使用,而sleep任何地方都可以. 3. wait无需捕捉异常,而sleep需要.(都抛出InterruptedException ,wait也需要捕获异常) 4. wait()无参数需要唤醒,线程状态WAITING;wait(1000L);到时间自己醒过来或者到时间之前被其他线程唤醒,状态和sleep都是TIME_WAITING 两者相同点:都会让渡CPU执行时间,等待再次调度!

    作者回复: 👍

    2019-03-12
    28
  • 郑晨Cc
    王老师 ABCD 那个例子真没看懂 线程1释放锁为啥会通知线程4?1和3才是互斥的啊 2和4互斥 按我的理解 3和4 不应该是在同一个等待队列里啊 因为不是通一把锁(准确来时不是同样的两把锁) 就着这个例子 我还有个关互斥锁的等待队列的问题 假设还是资源ABCD 线程5 获取AB 线程6获取CD 线程7试图获取AB 线程8试图获取BC 线程9试图获取CD 那线程 7,8,9 到底是不是在一个等待队列里面, JVM在实现 wait notify机制是时候到底存不存在真实的队列?

    作者回复: 都是this这一把锁: synchronized void apply(){} 所以是一个等待队列 就是500个线程,也是同一个等待队列,因为锁的都是this 队列一定是存在的

    2019-03-12
    8
    22
  • 陈志凯
    强烈建议老师每个章节配上完整的demo,包括模拟多线程多个客户操作的代码,这样看效果才是最佳的,我们自己也能根据代码实际好好观察!

    作者回复: 对于水平高的,完整的代码没必要。对于水平低的,完整的代码只能增加惰性。我就很讨厌粘贴一些无关的代码😃

    2019-03-12
    3
    15
收起评论
显示
设置
留言
99+
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部