一.前言
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有点差别哦