当前位置: 首页 > news >正文

底层源码和具体测试解析HotSpot JVM的notify唤醒有序性(5000字详解)

          在大家的认知里,或者大家也可能搜过,notify唤醒机制到底是随机的呢?还是顺序的呢?在网上其实也有很多人说notify的唤醒机制就是随机的,但实际上并不是这样的,notify的唤醒机制是先进先出的!

目录

源码注释说明

具体测试用例

测试用例一

测试用例二

底层原理解析

ObjectMonitor 的数据结构

wait源码

notify源码

 DequeueWaiter源码

  源码结论     


源码注释说明

让我们先来看看源码注释是怎么写的

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 wait methods.

翻译为:

唤醒正在等待此对象的监视器的单个线程。如果任何线程正在等待此对象,则选择其中一个线程进行唤醒。选择是任意的,由实现自行决定。线程通过调用其中一个 wait 方法等待对象的监视器。

      在注释中其实就已经说明了,选择是任意的,由实现自行决定的,既然注释都这样写了,那我们就先用测试用例测试一下。

具体测试用例

测试用例一

public class Main {private static final Object lock = new Object();private static List<String> awakingOrder = Collections.synchronizedList(new ArrayList<>());private static final int THREAD_COUNT = 100;private static CountDownLatch readyLatch = new CountDownLatch(THREAD_COUNT);private static CountDownLatch startLatch = new CountDownLatch(1);private static CountDownLatch finishLatch = new CountDownLatch(THREAD_COUNT);static class WaitingThread extends Thread {private String name;public WaitingThread(String name) {this.name = name;}@Overridepublic void run() {try {// 先通知主线程该线程已准备就绪readyLatch.countDown();// 等待主线程发出开始信号startLatch.await();synchronized (lock) {System.out.println(name + " 准备等待");lock.wait();awakingOrder.add(name);System.out.println(name + " 被唤醒");}} catch (InterruptedException e) {e.printStackTrace();} finally {finishLatch.countDown();}}}public static void main(String[] args) throws InterruptedException {List<WaitingThread> threads = new ArrayList<>();// 创建并启动100个线程for (int i = 1; i <= THREAD_COUNT; i++) {WaitingThread thread = new WaitingThread("线程" + i);threads.add(thread);thread.start();}// 等待所有线程准备就绪readyLatch.await();System.out.println("所有线程已准备就绪");// 发出开始信号,让所有线程开始等待startLatch.countDown();// 确保所有线程都进入等待状态Thread.sleep(1000);// 依次唤醒所有线程for (int i = 0; i < THREAD_COUNT; i++) {synchronized (lock) {System.out.println("开始第 " + (i + 1) + " 次唤醒");lock.notify();}// 给一点时间让被唤醒的线程执行完毕Thread.sleep(10);}// 等待所有线程执行完毕finishLatch.await();// 输出唤醒顺序System.out.println("唤醒顺序: " + awakingOrder);System.out.println("唤醒的线程总数: " + awakingOrder.size());}
}

       测试用例代码如上,我一开始看的时候还在想HotSpot JVM的的notify唤醒机制原来真的是顺序唤醒的,但是我再仔细看的时候发现并不是这样的!

        当我再往上看的时候发现,原来因为线程并发执行的原因导致并不是按照我原先设置的顺序获取到了synchronized锁!而导致了这个原因,经过对比其实可以发现,线程获取到锁等待的顺序是和被唤醒的顺序是一致的!所以HotSpot JVM的notify实现是先进先出的有序队列。

测试用例二

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;public class Main {private static final Object lock = new Object();private static List<String> awakingOrder = Collections.synchronizedList(new ArrayList<>());private static final int THREAD_COUNT = 100;private static volatile int currentThreadIndex = 0;static class WaitingThread extends Thread {private String name;private int index;private CountDownLatch prevLatch;private CountDownLatch myLatch;public WaitingThread(String name, int index, CountDownLatch prevLatch, CountDownLatch myLatch) {this.name = name;this.index = index;this.prevLatch = prevLatch;this.myLatch = myLatch;}@Overridepublic void run() {try {// 等待前一个线程就绪if (prevLatch != null) {prevLatch.await();}synchronized (lock) {System.out.println(name + " 准备等待,序号: " + index);// 通知下一个线程可以开始等待if (myLatch != null) {myLatch.countDown();}lock.wait();awakingOrder.add(name);System.out.println(name + " 被唤醒,序号: " + index);}} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) throws InterruptedException {List<WaitingThread> threads = new ArrayList<>();List<CountDownLatch> latches = new ArrayList<>();// 创建用于线程间同步的CountDownLatch数组for (int i = 0; i < THREAD_COUNT; i++) {latches.add(new CountDownLatch(1));}// 创建并启动线程for (int i = 0; i < THREAD_COUNT; i++) {CountDownLatch prevLatch = i == 0 ? null : latches.get(i - 1);CountDownLatch myLatch = latches.get(i);WaitingThread thread = new WaitingThread("线程" + (i + 1), i + 1, prevLatch, myLatch);threads.add(thread);thread.start();}// 等待一段时间确保所有线程都进入等待状态Thread.sleep(2000);System.out.println("开始按顺序唤醒线程");// 按顺序唤醒线程for (int i = 0; i < THREAD_COUNT; i++) {synchronized (lock) {System.out.println("准备唤醒第 " + (i + 1) + " 个线程");lock.notify();}// 给足够的时间让被唤醒的线程完成执行Thread.sleep(50);}// 等待所有线程完成for (Thread thread : threads) {thread.join();}// 输出唤醒顺序System.out.println("唤醒顺序: " + awakingOrder);System.out.println("唤醒的线程总数: " + awakingOrder.size());// 验证唤醒顺序是否正确boolean isOrdered = true;for (int i = 0; i < awakingOrder.size(); i++) {String expectedThread = "线程" + (i + 1);if (!awakingOrder.get(i).equals(expectedThread)) {isOrdered = false;break;}}System.out.println("唤醒顺序是否正确: " + isOrdered);}
}

       修改原来的代码 ,为每个线程创建一个 CountDownLatch,每个线程需要等待前一个线程进入等待状态才能继续,使用 prevLatch 和 myLatch 确保线程按顺序进入等待状态,这样就可以顺序的进入锁了,让我们再看下运行结果

 这样其实就可以清晰的看到这个唤醒的结果了,就是先进先出,顺序!

底层原理解析

现在让我们深入分析 HotSpot JVM 中的实现原理

ObjectMonitor 的数据结构

ObjectMonitor 的数据结构在 HotSpot 中,synchronized 的实现依赖于 ObjectMonitor 类,其结构如下:

ObjectMonitor() {_header       = NULL;_count        = 0;_waiters      = 0;_recursions   = 0;_object       = NULL;_owner        = NULL;_WaitSet      = NULL;    // 等待线程队列_WaitSetLock  = 0;_Responsible  = NULL;_succ         = NULL;_cxq          = NULL;FreeNext      = NULL;_EntryList    = NULL;    // 待竞争线程队列_SpinFreq     = 0;_SpinClock    = 0;
}

wait源码

让我们看看具体的wait等待实现

void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {// 将当前线程封装成 ObjectWaiter 对象ObjectWaiter node(THREAD);node.TState = ObjectWaiter::TS_WAIT;// 加入等待队列AddWaiter(&node);// 释放对象锁exit(true, THREAD);// 等待被唤醒Thread::SpinWait(node);
}

notify源码

再看看notify的唤醒实现 

void ObjectMonitor::notify(TRAPS) {// DequeueWaiter 方法会从 _WaitSet 队列头部取出一个等待线程ObjectWaiter* waiter = DequeueWaiter();if (waiter != NULL) {// 将等待线程移动到 EntryListExitExit(waiter);}
}

 DequeueWaiter源码

ObjectWaiter* ObjectMonitor::DequeueWaiter() {// 从 _WaitSet 队列头部获取第一个等待线程ObjectWaiter* waiter = _WaitSet;if (waiter != NULL) {_WaitSet = waiter->_next;waiter->_prev = NULL;waiter->_next = NULL;}return waiter;
}

  源码结论     

         从源码可以看出,wait() 的线程被放入 WaitSet 队列,notify() 总是唤醒 WaitSet 队列中的第一个线程,虽然 Java 规范说 notify 的选择是随机的,但在 HotSpot 实现中实际上是 FIFO(先进先出)的

      运行上面的测试代码,你也会发现,线程被唤醒的顺序与它们进入等待状态的顺序是一致的,多次运行结果都是一样的(在相同的 JVM 实现下),这验证了在 HotSpot JVM 中,notify() 确实是按照 FIFO 顺序唤醒线程的

      但需要注意的是:这个行为是 HotSpot JVM 的具体实现,其他 JVM 可能会有不同的实现

 

相关文章:

  • JimuBI 积木报表 v1.9.5发布,大屏和仪表盘,免费数据可视化
  • 当AI浏览器和AI搜索替代掉传统搜索份额时,老牌的搜索引擎市场何去何从。
  • ubuntu 日志文件清空方式的解读
  • Ubuntu22.04/24.04 P104-100 安装驱动和 CUDA Toolkit
  • FFmpeg之三 录制音频并保存, API编解码从理论到实战
  • C++初阶-STL简介
  • Unity 和 Unreal Engine(UE) 两大主流游戏引擎的核心使用方法
  • 司法大模型构建指南
  • 模方ModelFun工程操作教程
  • Deep Dark Sea 局域網文件共享即時匿名聊天去數據庫部署
  • 1、Linux操作系统下,ubuntu22.04版本切换中英文界面
  • mAh 与 Wh:电量单位的深度解析
  • 学习海康VisionMaster之路径提取
  • self-attention计算过程
  • JavaEE-多线程实战02
  • 计算机图形学(一):基础
  • err: Error: Request failed with status code 400
  • chrony服务器(2)
  • Azure Devops - 尝试一下在Pipeline中使用Self-hosted Windows agent
  • MongoDB与PHP7的集成与优化
  • 央行回应美债波动:单一市场、单一资产变动对我国外储影响总体有限
  • 人民日报头版:上海纵深推进浦东高水平改革开放
  • 我国已形成完整人工智能产业体系,专利申请量位居全球首位
  • 张家界乒乓球公开赛设干部职级门槛引关注,回应:仅限嘉宾组
  • 规范涉企案件审判执行工作,最高法今天发布通知
  • 体坛联播|巴萨“三杀”皇马夺国王杯,陈妤颉破亚洲少年纪录