从常见的一道面试题开始,题目的描述是这样子的:

有三个线程分别打印A、B、C,请用多线程编程实现,在屏幕上循环打印10次ABCABC…

网上大都教了你怎么去实现,其实我也写过一篇 https://blog.csdn.net/sanri1993/article/details/89644493 但是都没有把原理说透,说再多的解法别人也记不住。

这个其实需要从最原本的 Object 的方法 wait() ,notify() notifyAll() 来理解 ,想必读者在工作中应该几乎是没有使用过这几个方法的,这里我稍微介绍下功能

  • 这几个方法都必须在线程获得锁之后才能调用(这里说的锁是 synchronized 锁)
  • wait 方法会阻塞当前线程并释放锁,直到被唤醒。即另一个线程获得那把锁,并调用锁对象的 notify 或 notifyAll 方法
  • notify 调用后,wait 方法并不是立马往后执行,它需要重新获取锁
  • wait 调用后会把当前线程放进一个 wait 集合中,那个集合并不是有序的

利用 wait 的阻塞特性,我们可以用它来实现循环打印 ABC ;可以使用三把锁来实现,还需要一个变量来控制当前应该打印谁,当前如果不是打印这个值时,调用 wait,如果是打印这个值,就打印这个值并切换成一个打印变量,同时唤醒下一个打印,伪代码如下:

完整代码 ABCThreadWait这个地方

char currentChar = 'A';
Object lockA = new Object();
Object lockB = new Object();
Object lockC = new Object(); ThreadA
synchronized(lockA){
// 先获取 A 锁,可能马上就要调用 wait 方法
// 因为这里只有三个线程,所以用 if 和 while 是一样的,建议用 while
if(currentChar != 'A'){
lockA.wait();
}
print('A');currentChar = 'B';
//然后唤醒 B ,需要先获取B 锁
synchronized(lockB){
lockB.notify();
}
}
ThreadB
synchronized(lockB){
if(currentChar != 'B'){
lockB.wait();
}
print('B');currentChar = 'C';
synchronized(lockC){
lockC.notify();
}
}
ThreadC
synchronized(lockC){
if(currentChar != 'C'){
lockC.wait();
}
print('C');currentChar = 'A';
synchronized(lockA){
lockA.notify();
}
}

这里可以精简为使用一个线程类,使用不同的线程实例来实现,但还是使用的三把锁,每个线程都需要先获取自身的锁,然后判断是否需要打印,如果不是当前字符则释放锁;是当前字符就打印字符,在打印完后获取下一个打印的锁,把下一个打印线程唤醒。伪代码如下:

完整代码 ABCThreadWaitOneThreadClass这个地方

synchronized (lockSelf){
if(printChar != currentPrintChar){
try {
lockSelf.wait();
} catch (InterruptedException e) {e.printStackTrace();}
}
// 打印当前线程字符
System.out.print(printChar); // 切换下一个线程,并切换状态
if(currentPrintChar == 'A'){currentPrintChar = 'B';}
else if(currentPrintChar == 'B'){currentPrintChar = 'C';}
else if(currentPrintChar == 'C'){currentPrintChar = 'A';} //唤醒下一个线程
synchronized (lockNext) {
lockNext.notify();
}
}

当然也可以使用一把锁来实现,这需要用到 Lock + Condition ,关于 Lock 和 Condition 见这篇文章

使用 Condition 的 await signal signalAll 时,同样需要获得 Lock 锁,其它特性等同于 wait notify notifyAll

其实仔细看这道题,永远只有一个线程在打印,照理论来说只需要一把锁即可,上面需要有多把锁的原因是一个锁只有一个等待队列 ,并且 notify 也是随机唤醒的。而每个 Condition 会带一个等待队列,所以用 Condition 只要一把锁就可以了,减轻了代码的复杂度,多锁情况很容易造成死锁。

使用 Lock + Condition 的完整代码 ConditionABC这个地方 ,这是用多个线程类实现的,当然也可以用单个线程类多个线程实例来实现,这里就不再写了。

借用一篇写得挺不错的博文 ,请一定耐心把它读完再接着往下读 你真的懂 wait notify notifyAll 吗

文章中的源码QueueUseWaitNotify这个地方

这个图不错,收藏了

文章中有一个地方说得挺好,就是面试常问的 notify 和 notifyAll 的区别

青铜玩家会一脸纯真的看着面试官,就是唤醒一个和唤醒一堆啊,但它两真正的区别是 notifyAll 调用后,会把所有在 Wait Set 中的线程状态变成 RUNNABLE 状态,然后这些线程再去竞争锁,获取到锁的线程为 Run 状态,没有获取到锁的线程进入 Entry Set 集合中变成 Block 状态,它们只需要等到上个线程执行完或者 wait 就可以再次竞争锁而无需 notify ; 而 notify 方法只是照规则唤醒 Wait Set 中的某一个线程,其它的线程还是在 Wait Set 中。

文章中说到的为什么 wait 要写在 for 循环中是因为 wait 是释放了锁,然后阻塞,等到下次唤醒的时候,在多个生产者多个消费者的情况下,有可能是被 “同类” 唤醒的,所以需要再去检查下状态是否正确。

文章中有一个地方没有说明白 ,这里再解释下,就是那个使用 notfiy 会带来死锁的问题,个人理解,如有偏差望指正

当有多个消费者和多个生产者的时候,这时正好在消费,所以生产者是在 Wait Set 中,可能还有其它消费者也在 Wait Set 中,因为是 notify 而不是 notfiyAll 嘛,所以消费者有可能一直 notify 的都是另一个消费者,刚好这时 buffer 空了,正好所有消费都 wait 了而没能及时 notify 生产者,这时 Wait Set 中四目相望造成死锁。

文章最后有一个评论说可以生产者用一把锁,消费者用一把锁,这里也有实现 QueueUseWaitNofiy2

可以使用 Condition 做更好的实现,只使用一把锁,这里本身也只需要一把锁就可以了,具体实现见代码 QueueUseCondition

一点小推广

创作不易,希望可以支持下我的开源软件,及我的小工具,欢迎来 gitee 点星,fork ,提 bug 。

Excel 通用导入导出,支持 Excel 公式

博客地址:https://blog.csdn.net/sanri1993/article/details/100601578

gitee:https://gitee.com/sanri/sanri-excel-poi

使用模板代码 ,从数据库生成代码 ,及一些项目中经常可以用到的小工具

博客地址:https://blog.csdn.net/sanri1993/article/details/98664034

gitee:https://gitee.com/sanri/sanri-tools-maven

wait notify notifyAll await signal signalAll 的理解及示例的更多相关文章

  1. Java并发学习 & Executor学习 & 异常逃逸 & 同步互斥Best Practice & wait/notify, conditon#await/signal

    看了这篇文章:http://www.ciaoshen.com/2016/10/28/tij4-21/ 有一些Java并发的内容,另外查了一些资料. 朴素的Thread 首先,Java中关于线程Thre ...

  2. JUC在深入面试题——三种方式实现线程等待和唤醒(wait/notify,await/signal,LockSupport的park/unpark)

    一.前言 在多线程的场景下,我们会经常使用加锁,来保证线程安全.如果锁用的不好,就会陷入死锁,我们以前可以使用Object的wait/notify来解决死锁问题.也可以使用Condition的awai ...

  3. 使用ReentrantLock和Condition来代替内置锁和wait(),notify(),notifyAll()

    使用ReentrantLock可以替代内置锁,当使用内置锁的时候,我们可以使用wait() nitify()和notifyAll()来控制线程之间的协作,那么,当我们使用ReentrantLock的时 ...

  4. java 多线程 22 :生产者/消费者模式 进阶 利用await()/signal()实现

    java多线程15 :wait()和notify() 的生产者/消费者模式 在这一章已经实现了  wait/notify 生产消费模型 利用await()/signal()实现生产者和消费者模型 一样 ...

  5. java 并发——理解 wait / notify / notifyAll

    一.前言 前情简介: java 并发--内置锁 java 并发--线程 java 面试是否有被问到过,sleep 和 wait 方法的区别,关于这个问题其实不用多说,大多数人都能回答出最主要的两点区别 ...

  6. “全栈2019”Java多线程第三十三章:await与signal/signalAll

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  7. Java Object对象中的wait,notify,notifyAll的理解

    wait,notify,notifyAll 是定义在Object类的实例方法,用于控制线程状态,在线程协作时,大家都会用到notify()或者notifyAll()方法,其中wait与notify是j ...

  8. notify notifyAll 死锁

    从一个死锁分析wait,notify,notifyAll 泡芙掠夺者 关注 2017.08.24 22:00* 字数 1361 阅读 249评论 3喜欢 7赞赏 1 本文通过wait(),notify ...

  9. 使用Object的wait,notify,notifyAll做线程调度

    我们知道java中的所有类的祖先都是Object,Object类有四个个方法wait(),wait(long timeout),notify(),notifyAll(),这四个方法可以用来做线程的调度 ...

随机推荐

  1. 使用 element-ui 级联插件遇到的坑

    需求描述[省市区三级联动] 组件:Cascader 级联选择器 后端需要所选中的地区的名字,如:['北京市', '北京市', '东城区'] 获取后端省市区具体列表的接口返回数据: // 省 - 参数1 ...

  2. layaair和egret的区别

    egret缺点1 编译速度非常慢 2 就是强类型转换非常的麻烦 3 只能用ts 所以只能用他们的IDE 不能用sublime layaair唯一不足的就是 insepct太垃圾 占用游戏界面 所以建议 ...

  3. 基于docker实现redis高可用集群

    基于docker实现redis高可用集群 yls 2019-9-20 简介 基于docker和docker-compose 使用redis集群和sentinel集群,达到redis高可用,为缓存做铺垫 ...

  4. 用这个库 3 分钟实现让你满意的表格功能:Bootstrap-Table

    本文作者:HelloGitHub-kalifun 这是 HelloGitHub 推出的<讲解开源项目>系列,今天给大家推荐一个基于 Bootstrap 和 jQuery 的表格插件:Boo ...

  5. Spark性能优化指南——基础篇(转)

    [转]Spark性能优化指南——基础篇 http://mp.weixin.qq.com/s?__biz=MjM5NDMwNjMzNA==&mid=2651805828&idx=1&am ...

  6. mysql中 drop、truncate和delete的区别

    mysql中drop.truncate和delete的区别 (1)DELETE语句执行删除的过程是每次从表中删除一行,并且同时将该行的删除操作作为事务记录在日志中保存以便进行进行回滚操作. TRUNC ...

  7. 使用 layUI做一些简单的表单验证

    使用 layUI做一些简单的表单验证 <form method="post" class="layui-form" > <input name ...

  8. nyoj 98-成绩转换 (if, else if)

    98-成绩转换 内存限制:64MB 时间限制:3000ms 特判: No 通过数:49 提交数:74 难度:1 题目描述: 输入一个百分制的成绩M,将其转换成对应的等级,具体转换规则如下: 90~10 ...

  9. nyoj 113-字符串替换 (python replace, try ... except)

    113-字符串替换 内存限制:64MB 时间限制:3000ms 特判: No 通过数:31 提交数:71 难度:2 题目描述: 编写一个程序实现将字符串中的所有"you"替换成&q ...

  10. windows 10 上源码编译boost 1.66.0 | compile boost 1.66.0 from source on windows 10

    本文首发于个人博客https://kezunlin.me/post/854071ac/,欢迎阅读! compile boost 1.66.0 from source on windows 10 Ser ...