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是否匹配)。

  • 遇到竞争
    触发偏向锁撤销(需暂停线程),升级为轻量级锁。

轻量级锁的核心机制

  • 加锁过程

    1. 在栈帧中创建锁记录(Lock Record)

    2. 用CAS将对象头的Mark Word复制到锁记录中

    3. 再用CAS将对象头替换为指向锁记录的指针(成功则获取锁)

  • 解锁过程
    用CAS将Mark Word还原回对象头(若失败说明存在竞争,升级为重量级锁)。

 关键差异

  • 偏向锁:全程只需1次CAS(首次获取时)

  • 轻量级锁:每次进出同步块都需要CAS(加锁/解锁各1次)

5 锁消除

锁消除是JVM中一项重要的编译器优化技术,它通过移除不必要的同步操作来提升程序性能。这项技术主要解决"无实际竞争情况下的无效同步"问题。

锁消除基于逃逸分析(Escape Analysis) 技术:

  1. JVM在运行时分析对象的作用域

  2. 判断对象是否会"逃逸"出当前线程(即被其他线程访问)

  3. 如果确认对象不会逃逸(线程私有),则消除该对象的所有同步操作

public String concatStrings(String s1, String s2) {// StringBuilder是方法内部的局部变量StringBuilder sb = new StringBuilder();sb.append(s1);  // 内部有synchronized块sb.append(s2);  // 内部有synchronized块return sb.toString();
}
  1. StringBuilder实例sb是方法局部变量

  2. 逃逸分析确认sb不会逃逸出当前线程(不会被其他线程访问)

  3. JIT编译器会消除所有synchronized同步操作

6 wait /notify

首先涉及三个组件,Owner,EntryList,WaitSet。

组件存储线程状态触发条件是否持有锁位置转移方向
OwnerRUNNABLE成功获取锁→ WaitSet (wait()时)
EntryListBLOCKED竞争锁失败← WaitSet (notify()后)
WaitSetWAITING主动调用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() 的核心原理:

  1. 基于 Java 内置锁(synchronized)

  2. 使用等待-通知机制(wait/notify)

  3. 依赖 JVM 的线程终止通知

  4. 通过循环检查确保正确性

Thread.join() 通过 内置锁 确保线程安全,利用 等待-通知机制 实现阻塞与唤醒,依赖 JVM 的线程终止通知 自动触发唤醒,并通过 循环检查 防止虚假唤醒,最终实现线程间的有序协作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/web/82818.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Doris 与 Elasticsearch:谁更适合你的数据分析需求?

一、Doris 和 Elasticsearch 的基本概念 &#xff08;一&#xff09;Doris 是什么&#xff1f; Doris 是一个用于数据分析的分布式 MPP&#xff08;大规模并行处理&#xff09;数据库。它主要用于存储和分析大量的结构化数据&#xff08;比如表格数据&#xff09;&#xff0c…

使用Virtual Serial Port Driver+com2tcp(tcp2com)进行两台电脑的串口通讯

使用Virtual Serial Port Drivercom2tcp或tcp2com进行两台电脑的串口通讯 问题说明解决方案方案三具体操作流程网上教程软件安装拓扑图准备工作com2tcp和tcp2com操作使用串口助手进行验证 方案三存在的问题数据错误通讯延时 问题说明 最近想进行串口通讯的一个测试&#xff0c…

transformer和 RNN以及他的几个变体区别 改进

Transformer、RNN 及其变体&#xff08;LSTM/GRU&#xff09;是深度学习中处理序列数据的核心模型&#xff0c;但它们的架构设计和应用场景有显著差异。以下从技术原理、优缺点和适用场景三个维度进行对比分析&#xff1a; 核心架构对比 模型核心机制并行计算能力长序列依赖处…

CSS6404L 在物联网设备中的应用优势:低功耗高可靠的存储革新与竞品对比

物联网设备对存储芯片的需求聚焦于低功耗、小尺寸、高可靠性与传输效率&#xff0c;Cascadeteq 的 CSS6404L 64Mb Quad-SPI Pseudo-SRAM 凭借差异化技术特性&#xff0c;在同类产品中展现显著优势。以下从核心特性及竞品对比两方面解析其应用价值。 一、CSS6404L 核心产品特性…

go语言map扩容

map是什么&#xff1f; ​在Go语言中&#xff0c;map是一种内置的无序key/value键值对的集合&#xff0c;可以根据key在O(1)的时间复杂度内取到value&#xff0c;有点类似于数组或者切片结构&#xff0c;可以把数组看作是一种特殊的map&#xff0c;数组的key为数组的下标&…

2025年SDK游戏盾实战深度解析:防御T级攻击与AI反作弊的终极方案

一、引言&#xff1a;游戏安全的“生死防线” 2025年&#xff0c;全球游戏行业因DDoS攻击日均损失3.2亿元&#xff0c;攻击峰值突破8Tbps&#xff0c;且70% 的攻击为混合型&#xff08;DDoSCC&#xff09;。传统高防IP因延迟高、成本贵、协议兼容性差&#xff0c;已无法满足实…

【Linux】LInux下第一个程序:进度条

前言&#xff1a; 在前面的文章中我们学习了LInux的基础指令 【Linux】初见&#xff0c;基础指令-CSDN博客【Linux】初见&#xff0c;基础指令&#xff08;续&#xff09;-CSDN博客 学习了vim编辑器【Linux】vim编辑器_linux vim insert-CSDN博客 学习了gcc/g【Linux】编译器gc…

Web前端基础

### 一、浏览器 火狐浏览器、谷歌浏览器(推荐)、IE浏览器 推荐谷歌浏览器原因&#xff1a; 1、简洁大方,打开速度快 2、开发者调试工具&#xff08;右键空白处->检查&#xff0c;打开调试模式&#xff09; ### 二、开发工具 核心IDE工具 1. Visual Studio Code (VS Code)‌…

C++调试(肆):WinDBG分析Dump文件汇总

目录 1.前言 2.WinDBG中常用的指令 3.分析异常时要关注的信息 4.心得 前言 本篇博客主要针如何使用WinDBG工具调试Dump文件的流程进行一个讲解&#xff0c;具体捕获的Dump文件也是前两节例子中生成的Dump文件。 WinDBG中常用的指令 关于WinDBG调试时常用的指令主要分为以下几种…

SOC-ESP32S3部分:33-声学前端模型ESP-SR

飞书文档https://x509p6c8to.feishu.cn/wiki/YnbmwtqI5iBwE3kHA7AcZ3yTnLf ESP-SR 是乐鑫官方开发的一个音频组件&#xff0c;支持以下模块&#xff1a; 声学前端算法 AFE唤醒词检测 WakeNet命令词识别 MultiNet语音合成&#xff08;目前只支持中文&#xff09; 组件地址&am…

基于vscode,idea,java,html,css,vue,echart,maven,springboot,mysql数据库,在线考试系统

详细视频&#xff1a;【基于vscode,idea,java,html,css,vue,echart,maven,springboot,mysql数据库&#xff0c;在线考试系统-哔哩哔哩】 https://b23.tv/7hwmwmQ

【Linux】shell中的运行流程控制

目录 一.什么是运行流程控制 二.条件允许流程控制--if 2.1.单分支 2.2.双分支 2.3.多分支 if多分支练习 三.循环运行流程控制 无判定循环--for 判断循环--while&#xff0c;until 四.选择运行流程控制 五.自动应答--expect 5.1.固定位置的交互应答 5.2.非固定位置的…

新能源汽车热管理核心技术解析:冬季续航提升40%的行业方案

新能源汽车热管理核心技术解析&#xff1a;冬季续航提升40%的行业方案 摘要&#xff1a;突破续航焦虑的关键在热能循环&#xff01; &#x1f449; 本文耗时72小时梳理行业前沿方案&#xff0c;含特斯拉/比亚迪等8家车企热管理系统原理图 一、热管理为何成新能源车决胜关键&am…

OCR MLLM Evaluation

为什么需要评测体系&#xff1f;——背景与矛盾 ​​ 能干的事&#xff1a;​​ 看清楚发票、身份证上的字&#xff08;准确率>90%&#xff09;&#xff0c;速度飞快&#xff08;眨眼间完成&#xff09;。​​干不了的事&#xff1a;​​ 碰到复杂表格&#xff08;合并单元…

深入解析JVM工作原理:从字节码到机器指令的全过程

一、JVM概述 Java虚拟机(JVM)是Java平台的核心组件&#xff0c;它实现了Java"一次编写&#xff0c;到处运行"的理念。JVM是一个抽象的计算机器&#xff0c;它有自己的指令集和运行时内存管理机制。 JVM的主要职责&#xff1a; 加载&#xff1a;读取.class文件并验…

Python绘图库及图像类型之特殊领域可视化

Python绘图库及图像类型之基础图表-CSDN博客https://blog.csdn.net/weixin_64066303/article/details/148433762?spm1001.2014.3001.5501 Python绘图库及图像类型之高级可视化-CSDN博客https://blog.csdn.net/weixin_64066303/article/details/148450750?spm1001.2014.3001.…

04 APP 自动化- Appium toast 元素定位列表滑动

文章目录 一、toast 元素的定位二、滑屏操作 一、toast 元素的定位 toast 元素就是简易的消息提示框&#xff0c;toast 显示窗口显示的时间有限&#xff0c;一般3秒左右 # -*- codingutf-8 -*- from time import sleep from appium import webdriver from appium.options.an…

C/C++ OpenCV 矩阵运算

C/C OpenCV 矩阵运算详解 &#x1f4a1; OpenCV 是一个强大的开源计算机视觉和机器学习库&#xff0c;它提供了丰富的矩阵运算功能&#xff0c;这对于图像处理和计算机视觉算法至关重要。本文将详细介绍如何使用 C/C 和 OpenCV 进行常见的矩阵运算。 矩阵的创建与初始化 在进…

基于大模型的 UI 自动化系统

基于大模型的 UI 自动化系统 下面是一个完整的 Python 系统,利用大模型实现智能 UI 自动化,结合计算机视觉和自然语言处理技术,实现"看屏操作"的能力。 系统架构设计 #mermaid-svg-2gn2GRvh5WCP2ktF {font-family:"trebuchet ms",verdana,arial,sans-…

USB扩展器与USB服务器的2个主要区别

在现代办公和IT环境中&#xff0c;连接和管理USB设备是常见需求。USB扩展器&#xff08;常称USB集线器&#xff09;与USB服务器&#xff08;如朝天椒USB服务器&#xff09;是两类功能定位截然不同的解决方案。前者主要解决物理接口数量不足的“近身”连接扩展问题&#xff0c;而…