JUC并发编程(二)Monitor/自旋/轻量级/锁膨胀/wait/notify/锁消除
目录
一 基础
1 概念
2 卖票问题
3 转账问题
二 锁机制与优化策略
0 Monitor
1 轻量级锁
2 锁膨胀
3 自旋
4 偏向锁
5 锁消除
6 wait /notify
7 sleep与wait的对比
8 join原理
一 基础
1 概念
临界区
一段代码块内如果存在对共享资源的多线程读写操作,撑这段代码区为临界区。
竞态条件
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
为了避免临界区的竞态条件发生,有多种手段可以达到目的
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
2 卖票问题
代码实现:
首先代码实现了一个窗口类实现对卖票的相关业务逻辑,其次在主方法当中定义多个线程实现对票的购买,实现了sell方法的安全,random随机数的安全,集合的安全,同时利用.join方法等待所有线程执行结束。
package day01.mysafe;import java.util.ArrayList;
import java.util.List;import java.util.Vector;
import java.util.concurrent.ThreadLocalRandom;public class example1 {static int randomAmount() {return ThreadLocalRandom.current().nextInt(1, 6);}public static void main(String[] args) throws InterruptedException {//模拟卖票List<Integer> arr = new Vector<>();List<Thread> threads = new ArrayList<>();TicketWindow ticketWindow = new TicketWindow(1000);for (int i = 0; i < 2200; i++) {Thread t = new Thread(() -> {int num = ticketWindow.sell(randomAmount());arr.add(num);});threads.add(t);t.start();}//等待所有线程执行完for (Thread thread : threads) {thread.join();}System.out.println("剩余:" + ticketWindow.getAmount());System.out.println("卖出:" + arr.stream().mapToInt(x -> x == null ? 0 : x).sum());}
}/*** 窗口类*/
class TicketWindow {private int amount;public TicketWindow(int number) {this.amount = number;}public int getAmount() {return amount;}/*** 卖票*/public synchronized int sell(int amount) {if (this.amount >= amount) {this.amount -= amount;return amount;} else {return 0;}}
}
3 转账问题
加实例锁
锁的是当前对象,每个对象都有独立的锁,只影响一个实例的并发操作,多个实例可以并发进行。会出现死锁问题,当线程1 获取 a的锁,将a锁住需要修改b但是需要b的锁,此时需要等待b的锁,但是同时线程2获取b的锁,将b锁住需要修改a但是需要a的锁,两个线程相互等待,持续僵持导致死锁。
import java.util.concurrent.ThreadLocalRandom;public class example2 {static int random() {return ThreadLocalRandom.current().nextInt(1, 100);}public static void main(String[] args) throws InterruptedException {Amount a = new Amount(1000);Amount b = new Amount(1000);Thread t1 = new Thread(() -> {for (int i = 0; i < 100; i++) {a.transfer(b, random());}}, "t1");Thread t2 = new Thread(() -> {for (int i = 0; i < 100; i++) {b.transfer(a, random());}}, "t2");t1.start();t2.start();t1.join();t2.join();System.out.printf("余额为%d\n", b.getMoney() + a.getMoney());}}class Amount {private int money;public Amount(int money) {this.money = money;}public int getMoney() {return money;}public void setMoney(int money) {this.money = money;}//转账 (向a账户转账money元)public synchronized void transfer(Amount a, int money) {if (this.money >= money) {this.money -= money;a.money += money;}}}
加类锁
import java.util.concurrent.ThreadLocalRandom;public class example2 {static int random() {return ThreadLocalRandom.current().nextInt(1, 100);}public static void main(String[] args) throws InterruptedException {Amount a = new Amount(1000);Amount b = new Amount(1000);Thread t1 = new Thread(() -> {for (int i = 0; i < 100; i++) {a.transfer(b, random());}}, "t1");Thread t2 = new Thread(() -> {for (int i = 0; i < 100; i++) {b.transfer(a, random());}}, "t2");t1.start();t2.start();t1.join();t2.join();System.out.printf("余额为%d\n", b.getMoney()+a.getMoney());}}class Amount {private int money;public Amount(int money) {this.money = money;}public int getMoney() {return money;}public void setMoney(int money) {this.money = money;}//转账 (向a账户转账money元)public void transfer(Amount a, int money) {synchronized (Amount.class){if (this.money >= money) {this.money -= money;a.money += money;}}}}
二 锁机制与优化策略
0 Monitor
Monitor被翻译为监视器或管程。Monitor 是 JVM 实现 synchronized
的核心机制,通过 EntryList、WaitSet 和 Owner 管理线程对锁的访问。
当线程首次通过 synchronized
竞争 obj 的锁时,JVM 会在底层为其关联一个 Monitor,如果Owner没有对应的线程,则会成功获取线程锁,否则进入EntryList阻塞排队. (同一对象使用synchroized)
下面介绍线程持有锁并执行 wait/sleep 的运作状态(sleep可以在没有锁的状态运行,无锁就只释放CPU,有锁释放CPU,但锁不释放)
-
锁的争抢(进入EntryList)→ 持有(成为Owner)→ <wait> 主动让出,将锁释放(进入WaitSet)→ <notify> 唤醒后重新竞争->(进入EntryList)。
-
锁的争抢(进入EntryList)-->持有Owner -> <sleep> 主动让出CPU时间片,不释放锁,变为TIMED_WAITING状态同时维持Owner身份->时间结束后自动恢复运行,无需重新进入EntryList竞争。
Monitor 由以下核心组件构成:
- Owner(持有者):当前持有锁的线程。
- EntryList(入口队列):等待获取锁的线程队列。
- WaitSet(等待队列):调用
wait()
方法后释放锁的线程队列。
1 轻量级锁
轻量级锁:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就没有竞争),那么就可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,语法依旧是synchroized,不需要人工干预。
-
低竞争时用轻量级锁:当多线程竞争较小时(如交替执行同步代码),JVM 会优先使用轻量级锁(基于 CAS 操作),避免直接使用重量级锁(Monitor)的性能开销。
-
竞争加剧时升级:如果轻量级锁的 CAS 操作失败(其他线程同时竞争),JVM 会自动将其升级为重量级锁(通过操作系统互斥量实现阻塞)。
2 锁膨胀
锁膨胀是 JVM 在并发压力增大时,将轻量级锁升级为重量级锁的过程,以牺牲部分性能换取线程安全。
触发条件:
-
轻量级锁竞争失败:当多个线程同时竞争轻量级锁(CAS 操作失败),JVM 会将锁升级为重量级锁。
-
调用
wait()/notify()
:这些方法需要重量级锁(Monitor)的支持,会强制触发膨胀。 -
HashCode 冲突:若对象已计算哈希码,无法再使用偏向锁或轻量级锁,直接膨胀。
3 自旋
概念:自旋是“不停尝试”的锁获取策略
当首个线程获取轻量级锁后,第二个尝试访问的线程不会立即阻塞或促使锁升级,而是先进入自旋状态,等待原先的线程释放锁。若在自旋期间锁被释放,则该线程可直接获得锁,避免进入阻塞状态及触发锁升级至重量级锁,从而提高效率并减少资源消耗。这种机制有效降低了因锁升级带来的性能损耗,确保了在并发环境下的高效运行。
4 偏向锁
偏向锁是Java虚拟机(JVM)中一种针对同步操作的优化技术,主要用于减少无竞争情况下的同步开销。它是JVM锁升级机制的第一阶段(无锁→偏向锁→轻量级锁→重量级锁)。
在JDK15及以后版本,由于现代硬件性能提升和其他优化技术的出现,偏向锁默认被禁用,因为其带来的收益已经不明显,而撤销开销在某些场景下可能成为负担。
偏向锁与轻量级锁之间的对比
偏向锁 | 轻量级锁 |
---|---|
针对无竞争场景(同一线程多次获取锁) | 针对低竞争场景(多个线程交替执行,无并发冲突) |
消除整个同步过程的开销 | 避免操作系统互斥量(Mutex)的开销 |
偏向锁的核心机制
-
首次获取锁:
通过一次 CAS操作 将线程ID写入对象头的Mark Word,之后该线程进入同步块无需任何原子操作。 -
无竞争时:
执行同步代码就像无锁一样(仅检查线程ID是否匹配)。 -
遇到竞争:
触发偏向锁撤销(需暂停线程),升级为轻量级锁。
轻量级锁的核心机制
-
加锁过程:
-
在栈帧中创建锁记录(Lock Record)
-
用CAS将对象头的Mark Word复制到锁记录中
-
再用CAS将对象头替换为指向锁记录的指针(成功则获取锁)
-
-
解锁过程:
用CAS将Mark Word还原回对象头(若失败说明存在竞争,升级为重量级锁)。
关键差异:
偏向锁:全程只需1次CAS(首次获取时)
轻量级锁:每次进出同步块都需要CAS(加锁/解锁各1次)
5 锁消除
锁消除是JVM中一项重要的编译器优化技术,它通过移除不必要的同步操作来提升程序性能。这项技术主要解决"无实际竞争情况下的无效同步"问题。
锁消除基于逃逸分析(Escape Analysis) 技术:
-
JVM在运行时分析对象的作用域
-
判断对象是否会"逃逸"出当前线程(即被其他线程访问)
-
如果确认对象不会逃逸(线程私有),则消除该对象的所有同步操作
public String concatStrings(String s1, String s2) {// StringBuilder是方法内部的局部变量StringBuilder sb = new StringBuilder();sb.append(s1); // 内部有synchronized块sb.append(s2); // 内部有synchronized块return sb.toString();
}
-
StringBuilder
实例sb
是方法局部变量 -
逃逸分析确认
sb
不会逃逸出当前线程(不会被其他线程访问) -
JIT编译器会消除所有
synchronized
同步操作
6 wait /notify
首先涉及三个组件,Owner,EntryList,WaitSet。
组件 | 存储线程状态 | 触发条件 | 是否持有锁 | 位置转移方向 |
---|---|---|---|---|
Owner | RUNNABLE | 成功获取锁 | 是 | → WaitSet (wait()时) |
EntryList | BLOCKED | 竞争锁失败 | 否 | ← WaitSet (notify()后) |
WaitSet | WAITING | 主动调用wait() | 否 | → EntryList (被唤醒后) |
一个线程进入时首先会尝试获取Owner权,也就是获取锁,但是同一时刻只能有一个线程持有锁,获取成功可以直接执行临界代码区,获取失败的线程待在EntryList当中,处于Blocked阻塞状态,在持有锁阶段可以使用wait方法,会使当前锁释放,并进入WaitSet当中,处于Waiting等待状态,其必须使用notify/notifyAll唤醒才可进入EntryList当中,从而再次得到竞争Owner的权力。
代码示意
代码开启两个线程,对同一个对象实例加锁,线程1进入锁后,执行wait进入WaitSet进入阻塞等待,线程1将锁释放,此时线程2获取到锁,将线程1唤醒,线程1将后续代码执行结束。
- 在线程2当中睡眠3s一定程度上确保其在线程1之后执行(一定程度上避免出现永久阻塞等待的状态),线程1阻塞等待,线程2唤醒。
- 线程1被唤醒之后会将线程当中剩余的代码执行结束,然后进入EntryList中。
- wait可加参数,相当于设置一个超时时间,在这个期间中等待,超时自动释放。
- 在类文件当中加入一个Boolean标志位可以防止虚假唤醒的出现,虚假唤醒指的是在没有明确使用notify/notifyAll对线程进行唤醒的条件下而被唤醒或者唤醒的并不是想要的。(借助while循环持续判断)
package day01.mysynchronized;public class Example4 {static final Object obj = new Object();static boolean isSignaled = false; // 新增标志位public static void main(String[] args) {System.out.println("线程1开始执行");Thread t1 = new Thread(() -> {try {synchronized (obj) {System.out.println("线程1处于等待状态....");// 循环检查标志位,防止虚假唤醒while (!isSignaled) {obj.wait();}System.out.println("线程1执行结束");}} catch (InterruptedException e) {Thread.currentThread().interrupt();}}, "t1");Thread t2 = new Thread(() -> {System.out.println("线程2开始执行,睡眠3秒...");try {Thread.sleep(3000);} catch (InterruptedException e) {Thread.currentThread().interrupt();}synchronized (obj) {System.out.println("线程2对线程1进行唤醒");isSignaled = true; // 设置标志位为trueobj.notify();System.out.println("线程2执行结束,线程1被唤醒");}}, "t2");t2.start();t1.start();}
}
运行展示:
7 sleep与wait的对比
1 对于参数含义的不同
sleep(0)是一种主动让出时间片的过程,而wait(0) /wait() 是指长时间等待
2 调用位置的要求
sleep可以在任意位置调用,而wait必须在同步代码块当中调用。
3 唤醒机制
sleep:interrupt或者超时唤醒
wait:其他线程使用notify/notifyAll或者超时唤醒
4 线程改变的状态不同
线程持有锁并执行sleep,当前线程并不会释放当前持有的锁,而是携带锁休眠一段时间,持续处于Owner状态,休眠结束会继续执行代码逻辑。
线程持有并锁执行wait时,当前线程会释放当前持有的锁,并从持有管程Monitor转移到WaitSet等待队列当中,其他线程可以获取锁的持有权,可借助notify/notifyAll将锁唤醒,从WaitSet等待队列进入EntryList锁竞争队列当中。
8 join原理
join()
方法的实现基于 Java 的 等待-通知机制 和 线程状态管理
Thread.join()
的核心原理:
-
基于 Java 内置锁(synchronized)
-
使用等待-通知机制(wait/notify)
-
依赖 JVM 的线程终止通知
-
通过循环检查确保正确性
Thread.join()
通过 内置锁 确保线程安全,利用 等待-通知机制 实现阻塞与唤醒,依赖 JVM 的线程终止通知 自动触发唤醒,并通过 循环检查 防止虚假唤醒,最终实现线程间的有序协作。
相关文章:

JUC并发编程(二)Monitor/自旋/轻量级/锁膨胀/wait/notify/锁消除
目录 一 基础 1 概念 2 卖票问题 3 转账问题 二 锁机制与优化策略 0 Monitor 1 轻量级锁 2 锁膨胀 3 自旋 4 偏向锁 5 锁消除 6 wait /notify 7 sleep与wait的对比 8 join原理 一 基础 1 概念 临界区 一段代码块内如果存在对共享资源的多线程读写操作…...
SpringCloud优势
目录 完善的微服务支持 高可用性和容错性 灵活的配置管理 强大的服务网关 分布式追踪能力 丰富的社区生态 易于与其他技术栈集成 完善的微服务支持 Spring Cloud 提供了一整套工具和组件来支持微服务架构的开发,包括服务注册与发现、负载均衡、断路器、配置管理等功能…...
Electron简介(附电子书学习资料)
一、什么是Electron? Electron 是一个由 GitHub 开发的 开源框架,允许开发者使用 Web技术(HTML、CSS、JavaScript) 构建跨平台的桌面应用程序(Windows、macOS、Linux)。它将 Chromium浏览器内核 和 Node.j…...

深入理解 C++ 左值右值、std::move 与函数重载中的参数传递
在 C 编程中,左值和右值的概念以及std::move的使用,常常让开发者感到困惑。特别是在函数重载场景下,如何合理利用这些特性来优化代码性能、确保语义正确,更是一个值得深入探讨的话题。 在开始之前,先提出几个问题&…...
【大厂机试题解法笔记】矩阵匹配
题目 从一个 N * M(N ≤ M)的矩阵中选出 N 个数,任意两个数字不能在同一行或同一列,求选出来的 N 个数中第 K 大的数字的最小值是多少。 输入描述 输入矩阵要求:1 ≤ K ≤ N ≤ M ≤ 150 输入格式 N M K N*M矩阵 输…...

java 局域网 rtsp 取流 WebSocket 推送到前端显示 低延迟
众所周知 摄像头取流推流显示前端延迟大 传统方法是服务器取摄像头的rtsp流 然后客户端连服务器 中转多了,延迟一定不小。 假设相机没有专网 公网 1相机自带推流 直接推送到云服务器 然后客户端拉去 2相机只有rtsp ,边缘服务器拉流推送到云服务器 …...

免费批量Markdown转Word工具
免费批量Markdown转Word工具 一款简单易用的批量Markdown文档转换工具,支持将多个Markdown文件一键转换为Word文档。完全免费,无需安装,解压即用! 官方网站 访问官方展示页面了解更多信息:http://mutou888.com/pro…...
【Redis】Redis从入门到实战:全面指南
Redis从入门到实战:全面指南 一、Redis简介 Redis(Remote Dictionary Server)是一个开源的、基于内存的键值存储系统,它可以用作数据库、缓存和消息代理。由Salvatore Sanfilippo于2009年开发,因其高性能、丰富的数据结构和广泛的语言支持而广受欢迎。 Redis核心特点:…...
LeetCode 0386.字典序排数:细心总结条件
【LetMeFly】386.字典序排数:细心总结条件 力扣题目链接:https://leetcode.cn/problems/lexicographical-numbers/ 给你一个整数 n ,按字典序返回范围 [1, n] 内所有整数。 你必须设计一个时间复杂度为 O(n) 且使用 O(1) 额外空间的算法。…...
智能体革命:企业如何构建自主决策的AI代理?
OpenAI智能代理构建实用指南详解 随着大型语言模型(LLM)在推理、多模态理解和工具调用能力上的进步,智能代理(Agents)成为自动化领域的新突破。与传统软件仅帮助用户自动化流程不同,智能代理能够自主执行工…...

以太网PHY布局布线指南
1. 简介 对于以太网布局布线遵循以下准则很重要,因为这将有助于减少信号发射,最大程度地减少噪声,确保器件作用,最大程度地减少泄漏并提高信号质量。 2. PHY设计准则 2.1 DRC错误检查 首先检查DRC规则是否设置正确,然…...
linux设备重启后时间与网络时间不同步怎么解决?
linux设备重启后时间与网络时间不同步怎么解决? 设备只要一重启,时间又错了/偏了,明明刚刚对时还是对的! 这在物联网、嵌入式开发环境特别常见,尤其是开发板、树莓派、rk3588 这类设备。 解决方法: 加硬件…...

若依项目部署--传统架构--未完待续
若依项目介绍 项目源码获取 #Git工具下载 dnf -y install git #若依项目获取 git clone https://gitee.com/y_project/RuoYi-Vue.git项目背景 随着企业信息化需求的增加,传统开发模式存在效率低,重复劳动多等问题。若依项目通过整合主流技术框架&…...
零基础在实践中学习网络安全-皮卡丘靶场(第十一期-目录遍历模块)
经过前面几期的内容我们学习了很多网络安全的知识,而这期内容就涉及到了前面的第六期-RCE模块,第七期-File inclusion模块,第八期-Unsafe Filedownload模块。 什么是"遍历"呢:对学过一些开发语言的朋友来说应该知道&…...
mcts蒙特卡洛模拟树思想
您这个观察非常敏锐,而且在很大程度上是正确的!您已经洞察到了MCTS算法在不同阶段的两种不同行为模式。我们来把这个关系理得更清楚一些,您的理解其实离真相只有一步之遥。 您说的“select是在二次选择的时候起作用”,这个观察非…...

华为云Flexus+DeepSeek征文 | 基于Dify构建具备联网搜索能力的知识库问答助手
华为云FlexusDeepSeek征文 | 基于Dify构建具备联网搜索能力的知识库问答助手 一、构建知识库问答助手引言二、构建知识库问答助手环境2.1 基于FlexusX实例的Dify平台2.2 基于MaaS的模型API商用服务 三、构建知识库问答助手实战3.1 配置Dify环境3.2 创建知识库问答助手3.3 使用知…...
Qt学习及使用_第1部分_认识Qt---Qt开发基本流程
前言 学以致用,通过QT框架的学习,一边实践,一边探索编程的方方面面. 参考书:<Qt 6 C开发指南>(以下称"本书") 标识说明:概念用粗体倾斜.重点内容用(加粗黑体)---重点内容(红字)---重点内容(加粗红字), 本书原话内容用深蓝色标识,比较重要的内容用加粗倾…...
[特殊字符] Spring Boot底层原理深度解析与高级面试题精析
一、Spring Boot底层原理详解 Spring Boot的核心设计哲学是约定优于配置和自动装配,通过简化传统Spring应用的初始化和配置流程,显著提升开发效率。其底层原理可拆解为以下核心机制: 自动装配(Auto-Configuration) 核…...
MeanFlow:何凯明新作,单步去噪图像生成新SOTA
1.简介 这篇文章介绍了一种名为MeanFlow的新型生成模型框架,旨在通过单步生成过程高效地将先验分布转换为数据分布。文章的核心创新在于引入了平均速度的概念,这一概念的引入使得模型能够通过单次函数评估完成从先验分布到数据分布的转换,显…...
【2D与3D SLAM中的扫描匹配算法全面解析】
引言 扫描匹配(Scan Matching)是同步定位与地图构建(SLAM)系统中的核心组件,它通过对齐连续的传感器观测数据来估计机器人的运动。本文将深入探讨2D和3D SLAM中的各种扫描匹配算法,包括数学原理、实现细节以及实际应用中的性能对比,特别关注…...

【Vue】scoped+组件通信+props校验
【scoped作用及原理】 【作用】 默认写在组件中style的样式会全局生效, 因此很容易造成多个组件之间的样式冲突问题 故而可以给组件加上scoped 属性, 令样式只作用于当前组件的标签 作用:防止不同vue组件样式污染 【原理】 给组件加上scoped 属性后…...
Docker环境下安装 Elasticsearch + IK 分词器 + Pinyin插件 + Kibana(适配7.10.1)
做RAG自己打算使用esmilvus自己开发一个,安装时好像网上没有比较新的安装方法,然后找了个旧的方法对应试试: 🚀 本文将手把手教你在 Docker 环境中部署 Elasticsearch 7.10.1 IK分词器 拼音插件 Kibana,适配中文搜索…...
第14节 Node.js 全局对象
JavaScript 中有一个特殊的对象,称为全局对象(Global Object),它及其所有属性都可以在程序的任何地方访问,即全局变量。 在浏览器 JavaScript 中,通常 window 是全局对象, 而 Node.js 中的全局…...

构建Docker镜像的Dockerfile文件详解
文章目录 前言Dockerfile 案例docker build1. 基本构建2. 指定 Dockerfile 路径3. 设置构建时变量4. 不使用缓存5. 删除中间容器6. 拉取最新基础镜像7. 静默输出完整示例 docker runDockerFile 入门syntax指定构造器FROM基础镜像RUN命令注释COPY复制ENV设置环境变量EXPOSE暴露端…...
Shell 解释器 bash 和 dash 区别
bash 和 dash 都是 Unix/Linux 系统中的 Shell 解释器,但它们在功能、语法和性能上有显著区别。以下是它们的详细对比: 1. 基本区别 特性bash (Bourne-Again SHell)dash (Debian Almquist SHell)来源G…...

从0开始学习R语言--Day17--Cox回归
Cox回归 在用医疗数据作分析时,最常见的是去预测某类病的患者的死亡率或预测他们的结局。但是我们得到的病人数据,往往会有很多的协变量,即使我们通过计算来减少指标对结果的影响,我们的数据中依然会有很多的协变量,且…...

ABAP设计模式之---“Tell, Don’t Ask原则”
“Tell, Don’t Ask”是一种重要的面向对象编程设计原则,它强调的是对象之间如何有效地交流和协作。 1. 什么是 Tell, Don’t Ask 原则? 这个原则的核心思想是: “告诉一个对象该做什么,而不是询问一个对象的状态再对它作出决策。…...
Oracle实用参考(13)——Oracle for Linux物理DG环境搭建(2)
13.2. Oracle for Linux物理DG环境搭建 Oracle 数据库的DataGuard技术方案,业界也称为DG,其在数据库高可用、容灾及负载分离等方面,都有着非常广泛的应用,对此,前面相关章节已做过较为详尽的讲解,此处不再赘述。 需要说明的是, DG方案又分为物理DG和逻辑DG,两者的搭建…...
CentOS 7.9安装Nginx1.24.0时报 checking for LuaJIT 2.x ... not found
Nginx1.24编译时,报LuaJIT2.x错误, configuring additional modules adding module in /www/server/nginx/src/ngx_devel_kit ngx_devel_kit was configured adding module in /www/server/nginx/src/lua_nginx_module checking for LuaJIT 2.x ... not…...
IP选择注意事项
IP选择注意事项 MTP、FTP、EFUSE、EMEMORY选择时,需要考虑以下参数,然后确定后选择IP。 容量工作电压范围温度范围擦除、烧写速度/耗时读取所有bit的时间待机功耗擦写、烧写功耗面积所需要的mask layer...