别搞了,notify是顺序唤醒线程的

2022-08-23 14:25:55 浏览数 (1)

一.前言

hello,everyone.本周博主在公司给实习生做了多线程相关课程的培训。课后有个小兄弟问了我一道题目说,为什么睡眠时间放置的位子不一样,notify唤醒的线程顺序不一样。时而顺序唤醒,时而乱序唤醒。我当时看到题目,瞥了一眼说,一会儿看一下,心想小case。

回去之后看完题目才发现,事情没有这么简单,花了我不少功夫。

因此把这个知识点跟大家分享一下,如有不对之处,欢迎指正。

二.问题描述

2.1.代码

代码语言:javascript复制
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
​
public class notifyDemo {
    //等待列表
    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 {
        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) {
                //notify调用一次仅可以唤醒一个线程,唤醒多个线程需要调用多次
                lock.notify();
                //TimeUnit.MILLISECONDS.sleep(10);
            }
            TimeUnit.MILLISECONDS.sleep(10);
        }
        TimeUnit.SECONDS.sleep(1);
        System.out.println("wait顺序:" waitList.toString());
        System.out.println("唤醒顺序:" notifyList.toString());
    }
}

2.2.问题描述

关键代码

代码语言:javascript复制
for(int i=0;i<50;i  ){
            synchronized (lock) {
                lock.notify();
                //1
                TimeUnit.MILLISECONDS.sleep(10);
            }
            //2
            TimeUnit.MILLISECONDS.sleep(10);
        }

小实习生一开始为了验证notify是随机唤醒的等待线程的。

一开始注释2这里的等待时间是没有的,只有注释1处的代码。

输出结果为乱序打印,与jdk文档描述的notify随机唤醒线程一致

后面注释了1,加了2.

输出结果为顺序打印

三.探索问题

3.1.解题思路

一开始看到问题的时候,我以为是JVM做了编译优化:一个 println 竟然比 volatile 还好使?。后面我仔细看了代码感觉不像。就加了一句打印,也没什么变量可以优化。

然后看一下notify这个方法的注释

代码语言:javascript复制
/**
 * Wakes up a single thread that is waiting on this object's
 * monitor. If any threads are waiting on this object, one of them
 * is chosen to be awakened. The choice is arbitrary and occurs at
 * the discretion of the implementation. A thread waits on an object's
 * monitor by calling one of the {@code wait} methods.
 */

大概的意思就是说我是notify方法调用后会随机唤醒线程,但是具体的实现是有jvm决定。

到这里就有点思路了。

看一下hotspot的对于synchronized的实现。

发现内部实现了一个队列

这下明白了。wait方法与notify方法就是维护了一条先入先出的队列。保证了顺序性。

也就是说实际上在hotspot里面对notify方法的实现是顺序唤醒的

3.2.问题解析

那么上面代码中随机打印又是怎么一回事呢?

再看一下关键部分代码

代码语言:javascript复制
1.for(int i=0;i<50;i  ){
2.    synchronized (lock) {
3.         lock.notify();
4.       TimeUnit.MILLISECONDS.sleep(10);
5.    }
6.    TimeUnit.MILLISECONDS.sleep(10);
}

不注释6,注释4.

1.synchronized代码块调用lock.notify(),唤醒队列中等待的第一个线程

2.主线程释放对lock的锁,主线程进入等待

3.队列中第一个线程获得所进行打印。

4.主线程再次进入循环。。

不注释4,注释6.

1.synchronized代码块调用lock.notify(),唤醒队列中等待的第一个线程

2.主线程并未释放锁,继续执行4代码等待

3.等待时间到则释放锁

4.紧接着主线程进入循环再次调用synchronized去给lock对象加锁,此时唤醒的队列中等待的第一个线程也去争抢。

5.其实本质意义上就是主线程与被唤醒的锁进行锁的争抢,这里还有一个点可以证明,此种情况下,虽然结果是乱序的,但是看一下结果前面总是有序的,说明被唤醒的线程争抢到了锁进行了业务执行。后续一旦主线程在释放完之后又再次获取锁,再次唤醒新的线程,就会出现多个线程与主线程抢锁,产生线程竞争,拿到锁的线程不一致了,输出自然也不一致了。

四.总结

notify函数在jdk中定义为随机唤醒,但是具体实现取决于不同的虚拟机,想主流的hotspot就是使用队列进行维护等待与唤醒的线程,是顺序唤醒的。

这里感兴趣的同学可以再去试试看notifyAll()方法,输出满足顺序输出,但是与notify有点差别哦

jdk

0 人点赞