面试官:为什么 wait() 方法需要写在循环里?
点击上方“朱小厮的博客”,选择“设为星标”
后台回复"加群",加入新技术
来源:8rr.co/6cj5
问:为什么是 while 而不是 if ?
大多数人都知道常见的使用 synchronized 代码:
synchronized (obj) {while (check pass) {wait();}// do your business
}
那么问题是为啥这里是 while 而不是 if 呢?这个问题我最开始也想了很久,按理来说已经在 synchronized 块里面了嘛,就不需要了。这个也是我前面一直是这么认为的,直到最近看了一个 Stackoverflow 上的问题才对这个问题有了比较深入的理解。
试想我们要试想一个有界的队列。那么常见的代码可以是这样:
static class Buf {private final int MAX = 5;private final ArrayList<Integer> list = new ArrayList<>();synchronized void put(int v) throws InterruptedException {if (list.size() == MAX) {wait();}list.add(v);notifyAll();}synchronized int get() throws InterruptedException {// line 0 if (list.size() == 0) { // line 1wait(); // line2// line 3}int v = list.remove(0); // line 4notifyAll(); // line 5return v;}synchronized int size() {return list.size();}
}
注意到这里用的 if,那么我们来看看它会报什么错呢?
下面的代码用了 1 个线程来 put,10 个线程来 get:
final Buf buf = new Buf();
ExecutorService es = Executors.newFixedThreadPool(11);
for (int i = 0; i < 1; i++)
es.execute(new Runnable() {@Overridepublic void run() {while (true ) {try {buf.put(1);Thread.sleep(20);}catch (InterruptedException e) {e.printStackTrace();break;}}}
});
for (int i = 0; i < 10; i++) {es.execute(new Runnable() {@Overridepublic void run() {while (true ) {try {buf.get();Thread.sleep(10);}catch (InterruptedException e) {e.printStackTrace();break;}}}});
}es.shutdown();
es.awaitTermination(1, TimeUnit.DAYS);
这段代码很快或者说一开始就会报错:
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:653)
at java.util.ArrayList.remove(ArrayList.java:492)
at TestWhileWaitBuf.get(TestWhileWait.java:80)atTestWhileWait2.run(TestWhileWait.java:47)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
很明显,在 remove 的时候报错了。那么我们来分析下:
假设现在有 A,B 两个线程来执行 get 操作,我们假设如下的步骤发生了:
1. A 拿到了锁 line 0。
2. A 发现 size==0, (line 1),然后进入等待,并释放锁 (line 2)。
3. 此时 B 拿到了锁,line0,发现 size==0,(line 1),然后进入等待,并释放锁 (line 2)。
4. 这个时候有个线程 C 往里面加了个数据 1,那么 notifyAll 所有的等待的线程都被唤醒了。
5. AB 重新获取锁,假设又是 A 拿到了。然后他就走到 line 3,移除了一个数据,(line4) 没有问题。
6. A 移除数据后想通知别人,此时 list 的大小有了变化,于是调用了 notifyAll (line5),这个时候就把 B 给唤醒了,那么 B 接着往下走。
7. 这时候 B 就出问题了,因为其实此时的竞态条件已经不满足了 (size==0)。B 以为还可以删除就尝试去删除,结果就跑了异常了。
那么 fix 很简单,在 get 的时候加上 while 就好了:
synchronized int get() throws InterruptedException {while (list.size() == 0) {wait();}int v = list.remove(0);notifyAll();return v;}
同样的,我们可以尝试修改 put 的线程数和 get 的线程数来发现如果 put 里面不是 while 的话也是不行的。
我们可以用一个外部周期性任务来打印当前 list 的大小,你会发现大小并不是固定的最大5:
final Buf buf = new Buf();
ExecutorService es = Executors.newFixedThreadPool(11);
ScheduledExecutorService printer = Executors.newScheduledThreadPool(1);
printer.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {System.out.println(buf.size());}
}, 0, 1, TimeUnit.SECONDS);
for (int i = 0; i < 10; i++)
es.execute(new Runnable() {@Overridepublic void run() {while (true ) {try {buf.put(1);Thread.sleep(200);}catch (InterruptedException e) {e.printStackTrace();break;}}}
});
for (int i = 0; i < 1; i++) {es.execute(new Runnable() {@Overridepublic void run() {while (true ) {try {buf.get();Thread.sleep(100);}catch (InterruptedException e) {e.printStackTrace();break;}}}});
}es.shutdown();
es.awaitTermination(1, TimeUnit.DAYS);
这里我想应该说清楚了为啥必须是 while 还是 if 了。
问:什么时候用 notifyAll 或者 notify?
大多数人都会这么告诉你,当你想要通知所有人的时候就用 notifyAll,当你只想通知一个人的时候就用 notify。但是我们都知道 notify 实际上我们是没法决定到底通知谁的(都是从等待集合里面选一个)。那这个还有什么存在的意义呢?
在上面的例子中,我们用到了 notifyAll,那么下面我们来看下用 notify 是否可以工作呢?
synchronized void put(int v) throws InterruptedException {if (list.size() == MAX) {wait();}list.add(v);notify();}synchronized int get() throws InterruptedException {while (list.size() == 0) {wait();}int v = list.remove(0);notify();return v;}
下面的几点是 jvm 告诉我们的:
任何时候,被唤醒的来执行的线程是不可预知。比如有 5 个线程都在一个对象上,实际上我不知道 下一个哪个线程会被执行。
synchronized 语义实现了有且只有一个线程可以执行同步块里面的代码。
那么我们假设下面的场景就会导致死锁:
P – 生产者 调用 put。
C – 消费者 调用 get。
1. P1 放了一个数字1。
2. P2 想来放,发现满了,在wait里面等了。
3. P3 想来放,发现满了,在 wait 里面等了。
4. C1 想来拿,C2,C3 就在 get 里面等着。
5. C1 开始执行,获取1,然后调用 notify 然后退出。
如果 C1 把 C2 唤醒了,所以P2 (其他的都得等)只能在put方法上等着。(等待获取synchoronized (this) 这个monitor)。
C2 检查 while 循环发现此时队列是空的,所以就在 wait 里面等着。
C3 也比 P2 先执行,那么发现也是空的,只能等着了。
6. 这时候我们发现 P2、C2、C3 都在等着锁,最终 P2 拿到了锁,放一个 1,notify,然后退出。
7. P2 这个时候唤醒了P3,P3发现队列是满的,没办法,只能等它变为空。
8. 这时候没有别的调用了,那么现在这三个线程(P3, C2,C3)就全部变成 suspend 了,也就是死锁了。
想知道更多?扫描下面的二维码关注我后台回复”加群“获取公众号专属群聊入口
【原创系列 | 精彩推荐】
Paxos、Raft不是一致性算法嘛?
越说越迷糊的CAP
分布式事务科普——初识篇
分布式事务科普——终结篇
面试官居然问我Raft为什么会叫做Raft!
面试官给我挖坑:URI中的//有什么用
面试官给我挖坑:a[i][j]和a[j][i]有什么区别?
面试官给我挖坑:单机并发TCP连接数到底有多少?
网关Zuul科普
网关Spring Cloud Gateway科普
Nginx架构原理科普
OpenResty概要及原理科普
微服务网关 Kong 科普
云原生网关Traefik科普
点个在看少个 bug ????
面试官:为什么 wait() 方法需要写在循环里?相关推荐
- Java并发编程—为什么 wait() 方法需要写在 while 里,而不是 if?
原文作者:后端面试那些事儿 原文地址:再见面试官:为什么 wait() 方法需要写在 while 里,而不是 if? 问:为什么是 while 而不是 if ? 问:什么时候用 notifyAll 或 ...
- atoi函数_吊打面试官 | 腾讯经典考点写代码实现atoi函数
点击蓝字关注我哦 以下是本期干货视频视频后还附有文字版本哦 ▼<腾讯经典考点-写代码实现atoi函数>▼ ps:请在WiFi环境下打开,如果有钱任性请随意 在腾讯面试时,经常会被问到如何用 ...
- 面试官让我用channel实现sync包里的同步锁,是不是故意为难我?
前言 Go语言提供了channel和sync包两种并发控制的方法,每种方法都有他们适用的场景,并不是所有并发场景都适合应用channel的,有的时候用sync包里提供的同步原语更简单.今天这个话题纯属 ...
- 面试官:看你简历写了熟悉Kafka,它为什么速度会这么快?
前言 Kafka的消息是保存或缓存在磁盘上的,一般认为在磁盘上读写数据是会降低性能的,因为寻址会比较消耗时间,但是实际上,Kafka的特性之一就是高吞吐率. 即使是普通的服务器,Kafka也可以轻松支 ...
- 面试官:能不能手写一个 Promise?
大家好,我是若川.最近组织了源码共读活动,感兴趣的可以点此加我微信ruochuan12 进群参与,每周大家一起学习200行左右的源码,共同进步.已进行4个月了,很多小伙伴表示收获颇丰. 以下问题你是不 ...
- 面试官:你简历中写用过docker,能说说容器和镜像的区别吗?
点击上方"方志朋",选择"设为星标" 做积极的人,而不是积极废人 作者 | bethal 来源 | http://sina.lt/gfmf 这篇文章希望能够帮助 ...
- 面试官:听说你sql写的挺溜的,你说一说查询sql的执行过程
来自:非科班的科班 当希望Mysql能够高效的执行的时候,最好的办法就是清楚的了解Mysql是如何执行查询的,只有更加全面的了解SQL执行的每一个过程,才能更好的进行SQl的优化. 当执行一条查询的S ...
- 面试官让我用Flex写色子布局,我直接给写了6个
- 手写一个简单的HashMap,搞定挑剔面试官
作者:编程十二 链接:https://www.jianshu.com/p/1be0e957baf2 前言 今天去面试啊,聊得差不多的时候面试官突然问我会手写HashMap吗?这我哪能怂啊,好死不死的面 ...
最新文章
- mysql免费框架_瞧一瞧~看一看~MyCat架构剖析免费不要钱!(上)
- 实验5 编写调试有多个段的程序
- Python正则表达式指南
- java maven 没有target_Maven最全知识点总结 可以收藏啦
- 智能小车37:异常在ARM、JAVA、硬件里的实现
- java 拖放文字_myeclipse2014如何实现jsp中的html代码的文字拖放
- [转载] Java-forEach增强for循环是值传递规则详解
- python三元表达式求值_python 三元表达式的 列表推导式 生成器推导式
- DEBUG -- CLOSE BY CLIENT STACK TRACE问题的两种解决方案,整理自网络
- k8s dashboard_ASP.NET Core on K8S深入学习(2)部署过程解析与部署Dashboard
- Word转换PDF:pdf虚拟打印机怎么用操作技巧详解
- C语言小知识——uthash使用
- MIDI文件基础及使用Python库mido操作MIDI文件
- 机器人动力学与控制_机器人领域值得一看的好书推荐
- android12适配机型,安卓12支持机型有哪些?安卓12系统为什么有的软件用不了?...
- matlab仿真超声波测距,超声波测距仪制作-Arduino中文社区 - Powered by Discuz!
- Apache JMeter 5.1.1 Win 10 环境变量配置
- flag{e2f34a3a-9972-4ba5-bdeb-ff7d524d87cb} preg_match implode
- 足球视频AI(五)——球员与球的对象跟踪
- 数字人民币来了,它到底是什么?
热门文章
- cfg桩设备型号_试桩、试验桩、工程桩是一回事吗?
- html桌面刷新,桌面不能自动刷新怎么办
- multiprocessing.manager管理的对象需要加锁吗_iOS内存管理布局-理论篇
- koa2 mysql增删改查_react+koa2+mysql零门槛的全栈体验,附上完整项目分享
- 国家文物局:长城沿线群众是文物保护的重要力量
- 面向对象进阶------内置函数 str repr new call 方法
- 人工智能,不止于技术的革命--WOT2017全球创新技术峰会开幕
- corosync+pacemaker在centos7上的安装,配置简述
- 为@RequestMapping标注的方法扩展其传入参数
- Google C++ Coding Style:右值引用(Rvalue Reference)