大佬问我: notify()是随机唤醒线程么?
我的内心戏: 这不是显而易见么! 肯定是啊! jdk关于notify()注释都写的很清楚!
不过这么简单的问题?
机智如我, 决定再次装小小白, 回答: 不是!
大佬: 很好, 小伙子你真的让我刮目相看了!!
我:
大佬: 说说为什么?
我: ………………
牢不可破的知识点被大佬一问, 瞬间感觉哪里有点问题!
于是, 咸鱼君开启了求证模式.
(大佬问我不懂的也就算了, 问这种“共识”的, 我一定举出例子驳倒他!)
代码求证
身为码农, 我决定写代码先验证下!
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class NotifyTest{
//等待列表, 用来记录等待的顺序
private static List<String> waitList = new LinkedList<>();
//唤醒列表, 用来唤醒的顺序
private static List<String> notifyList = new LinkedList<>();
private static Object lock = new Object();
public static void main(String[] args) throws InterruptedException{
//创建50个线程
for(int i=0;i<50;i++){
String threadName = Integer.toString(i);
new Thread(() -> {
synchronized (lock) {
String cthreadName = Thread.currentThread().getName();
System.out.println("线程 ["+cthreadName+"] 正在等待.");
waitList.add(cthreadName);
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 ["+cthreadName+"] 被唤醒了.");
notifyList.add(cthreadName);
}
},threadName).start();
TimeUnit.MILLISECONDS.sleep(50);
}
TimeUnit.SECONDS.sleep(1);
for(int i=0;i<50;i++){
synchronized (lock) {
lock.notify();
TimeUnit.MILLISECONDS.sleep(10);
}
}
TimeUnit.SECONDS.sleep(1);
System.out.println("wait顺序:"+waitList.toString());
System.out.println("唤醒顺序:"+notifyList.toString());
}
}
代码很简单, 创建了50个线程, 对其wait()和notify(), 同时使用waitList和notifyList来记录各自的顺序!
跑一下代码
没任何悬念, 结果不就是证明了notify()是随机唤醒线程的么?!!
我信心爆棚, 喊着大佬来看(虽然没啥炫耀的, 但是能圆下“指导”大佬的梦想!)
大佬看了下代码, 然后看着我, 微微一笑
我内心一慌: 难道有问题?
只见大佬默默的拿起我的鼠标, 剪切,粘贴
了一行代码
for(int i=0;i<50;i++){
synchronized (lock) {
lock.notify();
//大佬把这行代码移出了synchronized{}
//TimeUnit.MILLISECONDS.sleep(10);
}
TimeUnit.MILLISECONDS.sleep(10);
}
TimeUnit.SECONDS.sleep(1);
大佬再次微微一笑
: 你再跑跑看!
看到大佬的自信从容, 我越来越慌,
赶紧运行下
看到这不可置信
的结果, 我彻底慌了
什么?!! 这到底这么回事? 改动了一行代码, notify()居然有序了?!!!
看着结果, 我沉思, 连大佬走了都没注意.
究竟哪里出了问题? 难道notify()真是有序唤醒的?
于是,有了接下来的文章!
有疑问的小伙伴不妨看下去!(大佬可以退散了)
代码问题分析
我们先分析下求证的代码.
只是移动了下sleep()语句, 结果居然天差地别?!
其实问题就出在sleep()上, 准确的是sleep()在synchronized里面还是外面.
当我们执行notify之后,由于sleep在symchronized内部, 因此没有释放锁!
(其实这点 大佬问我: notify()会立刻释放锁么?提起过)
lock.wait后 被通知到的线程,就会进入waitSet队列;
之后我们循环时
for(int i=0;i<50;i++){
synchronized (lock) {
lock.notify();
TimeUnit.MILLISECONDS.sleep(10);
}
TimeUnit.SECONDS.sleep(1);
}
lock.notify();去唤醒等待线程, 我们假设唤醒了线程A;
但是因为后面还要执行TimeUnit.MILLISECONDS.sleep(10)所以lock锁并没有被释放!
当TimeUnit.MILLISECONDS.sleep(10)执行完毕后, lock被释放,
此时被唤醒的线程A想获取lock,
但是我们的for循环中synchronized (lock)也想继续获取lock,
于是两者发生了锁竞争.
由于synchronized实际上不是公平锁,其锁竞争的机制具有随机性
这就导致了最终, 我们看到的结果好像是随机的!
当我们把TimeUnit.MILLISECONDS.sleep(10);移出synchronized同步块后
for(int i=0;i<50;i++){
synchronized (lock) {
lock.notify();
}
TimeUnit.MILLISECONDS.sleep(10);
}
TimeUnit.SECONDS.sleep(1);
lock锁立即被释放了,
并且紧跟的 TimeUnit.SECONDS.sleep(1)确保被唤醒的线程能够获得lock锁立刻执行,
所以, 我们看到的结果才是正确的!
理论求证
想通了代码, 得到了“notify是顺序唤醒
”的结果后,
不禁疑惑,
既然“notify是顺序唤醒”的, 那为什么广为流传的, 深入人心的确实“notify()是随机唤醒线程
”,
JDK开发大佬不可能犯这样的错吧?!
带着这样的疑惑, 咸鱼君选择了看JDK源码来求证!
这里以常用的JDK1.8源码为例
找到“notify()”源码, 看到了这段源码注释
翻译一下, 大致意思就是:
notify在源码的注释中说到notify选择唤醒的线程是任意的,但是依赖于具体实现的jvm.
看完后, 咸鱼君顿时茅塞顿开!
我们都知道, JVM有很多实现, 比较流行的就是hotspot!
带着质疑, 我们不妨接下去看看jdk1.8, hotspot中对于notify()究竟是如何实现的
synchronized的wait和notify是位于ObjectMonitor.cpp中
notify过程调用的是DequeueWaiter方法:
这里实际上是将_WaitSet中的第一个元素进行出队操作,
这也说明了notify是个顺序操作, 具有公平性.
看完源码, 我们不难得出结论,
原来hotspot对notofy()的实现并不是我们以为的随机唤醒, 而是“先进先出”的顺序唤醒!
此刻, 我对大佬钦佩不已!
同时明白了两个道理:
1. 广为流传的知识不一定是正确的, 有条件一定多看源码, 敢于质疑求证
2. 一定要注意版本, 没有知识是一成不变的!
欢迎关注我
技术公众号 “CTO技术”