13.1 大佬问我: notify()是随机唤醒线程么?

大佬问我: notify()是随机唤醒线程么?

我的内心戏: 这不是显而易见么! 肯定是啊! jdk关于notify()注释都写的很清楚!
不过这么简单的问题?

image

机智如我, 决定再次装小小白, 回答: 不是!

大佬: 很好, 小伙子你真的让我刮目相看了!!

:

image

大佬: 说说为什么?

: ………………

image

牢不可破的知识点被大佬一问, 瞬间感觉哪里有点问题!

于是, 咸鱼君开启了求证模式.

(大佬问我不懂的也就算了, 问这种“共识”的, 我一定举出例子驳倒他!)

代码求证

身为码农, 我决定写代码先验证下!

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来记录各自的顺序!

跑一下代码


image.png

没任何悬念, 结果不就是证明了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);

大佬再次微微一笑: 你再跑跑看!

image

看到大佬的自信从容, 我越来越慌,

image

赶紧运行下

image.png

看到这不可置信的结果, 我彻底慌了

image.png

什么?!! 这到底这么回事? 改动了一行代码, 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()”源码, 看到了这段源码注释

image.png

翻译一下, 大致意思就是:

notify在源码的注释中说到notify选择唤醒的线程是任意的,但是依赖于具体实现的jvm.

看完后, 咸鱼君顿时茅塞顿开!

我们都知道, JVM有很多实现, 比较流行的就是hotspot!

带着质疑, 我们不妨接下去看看jdk1.8, hotspot中对于notify()究竟是如何实现的

synchronized的wait和notify是位于ObjectMonitor.cpp中

image.png

notify过程调用的是DequeueWaiter方法:

image.png

这里实际上是将_WaitSet中的第一个元素进行出队操作,
这也说明了notify是个顺序操作, 具有公平性.

看完源码, 我们不难得出结论,

原来hotspot对notofy()的实现并不是我们以为的随机唤醒, 而是“先进先出”的顺序唤醒!

此刻, 我对大佬钦佩不已!

image

同时明白了两个道理:

1. 广为流传的知识不一定是正确的, 有条件一定多看源码, 敢于质疑求证

2. 一定要注意版本, 没有知识是一成不变的!

欢迎关注我

技术公众号 “CTO技术”

订阅号.png