jvm 锁升级机制

  Java 虚拟机(JVM)中的锁升级机制(也称为锁膨胀)是 HotSpot 虚拟机为了优化 synchronized 关键字的性能而引入的一项重要技术。它的核心思想是:根据实际遇到的竞争激烈程度,动态地将锁从开销最小的状态逐步升级到开销更大的状态,从而在无竞争或低竞争时减少锁操作的开销,而在高竞争时保证必要的互斥性和线程调度能力。

锁的状态主要有四种,升级路径如下:

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

锁只能升级(膨胀),不能降级(虽然理论上重量级锁在竞争消失后可以降级,但HotSpot 实现中为了简化,很少进行降级,尤其是从重量级锁降级)。

1. 无锁状态

  • 初始状态: 当一个对象刚被创建出来,且没有任何线程尝试获取它的锁时,它就处于无锁状态。
  • 特点: 没有锁的开销。
  • 适用场景: 对象从未被同步访问或只被单个线程访问(无需同步)。

2. 偏向锁

  • 设计目标: 优化同一个线程重复进入同步块的场景(无实际竞争)。消除在无竞争情况下的同步原语开销(如 CAS)。
  • 工作原理:
    1. 当第一个线程(T1)访问同步块时,JVM 会检查对象头中的 Mark Word。
    2. 如果当前是无锁状态,JVM 使用 CAS 操作尝试将 Mark Word 中的线程 ID 设置为 T1 的 ID,并将锁标志位设置为偏向模式。
    3. 如果 CAS 成功,T1 就持有了该对象的偏向锁。后续只要 T1 进入这个同步块,无需再进行任何同步操作(如 CAS 或锁申请),只需简单检查对象头中的线程 ID 是否还是自己。
  • 升级触发:
    • 竞争出现: 当另一个线程(T2)尝试获取这个已经被偏向于 T1 的锁时,偏向锁就会失效。
    • JVM 会撤销偏向锁。撤销过程需要等待持有偏向锁的线程(T1)到达全局安全点(Safepoint),暂停 T1。
    • 检查 T1 的状态:
      • 如果 T1 已经退出同步块(不再持有锁),则将对象头设置为无锁状态(或者根据情况尝试重新偏向给 T2)。
      • 如果 T1 仍在同步块中,则将锁升级为轻量级锁。JVM 会在 T1 的栈帧中创建一个锁记录(Lock Record),并将对象头的 Mark Word 复制到该锁记录中(称为 Displaced Mark Word),然后用 CAS 操作将对象头指向 T1 栈帧中的锁记录地址(轻量级锁状态)。
  • 特点: 适用于只有一个线程反复访问同步块的场景。加锁解锁几乎无额外开销。撤销偏向锁有代价(需要暂停线程)。
  • 关闭偏向锁: 由于偏向锁在存在竞争时撤销有开销,且现代应用中共享数据竞争往往更常见,从 JDK 15 开始,偏向锁默认被禁用(可通过 -XX:+UseBiasedLocking 开启,但已不推荐)。

3. 轻量级锁

  • 设计目标: 优化多个线程交替执行同步块,但未发生真正并发竞争的场景(低竞争)。避免直接使用重量级锁带来的操作系统内核态切换的开销。
  • 工作原理:
    1. 当线程尝试获取轻量级锁时(可能是从无锁升级而来,也可能是从偏向锁撤销升级而来),JVM 会在当前线程的栈帧中创建一个锁记录空间。
    2. 将对象头的 Mark Word 复制到该锁记录中(称为 Displaced Mark Word)。
    3. 然后线程尝试使用 CAS 操作将对象头中的 Mark Word 替换为指向该锁记录的指针。
      • 如果 CAS 成功,当前线程获得轻量级锁。锁标志位变为 00
      • 如果 CAS 失败(说明对象头已被其他线程修改,即发生了竞争),当前线程会自旋(循环尝试 CAS)一小段时间(自适应自旋)。
        • 如果在自旋期间成功获取到锁,则继续执行。
        • 如果自旋结束仍未成功,或者自旋过程中竞争加剧(如又有新线程加入竞争),则锁升级为重量级锁
  • 解锁过程: 使用 CAS 操作将 Displaced Mark Word 替换回对象头。
    • 如果成功,解锁完成。
    • 如果失败(说明锁已经膨胀为重量级锁),则在释放锁的同时唤醒等待线程。
  • 特点: 使用 CAS 和自旋代替互斥量,避免了用户态到内核态的切换,适用于线程阻塞时间非常短的场景(“忙等”)。自旋会消耗 CPU。如果锁持有时间较长或竞争激烈,自旋会浪费 CPU,性能反而下降。
  • 升级触发: CAS 失败且自旋获取锁失败(或自适应策略判断竞争激烈)、调用 wait() 方法(因为 wait() 需要重量级锁的监视器模型支持)。

4. 重量级锁

  • 设计目标: 处理高并发、激烈竞争的场景。保证在任意时刻只有一个线程能进入同步块。
  • 工作原理:
    • 当锁升级到重量级锁时,对象头中的 Mark Word 会指向一个与对象关联的监视器锁(Monitor,也称为管程或互斥锁),这个结构通常存在于堆中。
    • 该 Monitor 内部维护了一个入口队列(Entry Set) 和一个等待队列(Wait Set)
    • 当一个线程尝试获取重量级锁时:
      • 如果锁可用(未被持有),则获取成功,成为锁的持有者。
      • 如果锁已被其他线程持有,则当前线程会被阻塞(Park),并被操作系统挂起,放入入口队列等待唤醒。这涉及到用户态到内核态的切换(线程上下文切换),开销最大。
    • 当持有锁的线程释放锁时,它会唤醒入口队列中的某个或所有等待线程(具体策略取决于实现,如公平/非公平),被唤醒的线程会重新尝试获取锁。
  • 特点: 真正的互斥锁。阻塞线程,不消耗 CPU 空转。适用于竞争激烈或临界区执行时间较长的场景。线程阻塞、唤醒、上下文切换开销很大。
  • 升级触发: 轻量级锁自旋失败、调用 Object.wait() 方法(强制升级)。

总结锁升级机制

锁状态目标场景核心机制优点缺点升级触发条件
无锁无同步访问-无开销无法提供线程安全首次线程访问
偏向锁单线程重复访问CAS 设置 Thread ID同一线程后续进入无开销竞争时撤销开销大(需暂停线程)第二个线程尝试获取锁
轻量级锁低竞争(交替执行)CAS + 自旋 (栈锁记录)避免内核切换,开销小自旋消耗 CPU,长时间竞争性能下降CAS失败且自旋失败 / 调用 wait()
重量级锁高竞争操作系统 Monitor阻塞线程,不消耗 CPU 空转阻塞/唤醒开销大(内核切换)轻量级锁升级失败 / 显式调用 wait()

关键点

  1. 自适应自旋: 轻量级锁中的自旋次数不是固定的,JVM 会根据之前在该锁上的自旋成功情况以及持有者的状态,动态调整自旋时间(适应性自旋)。
  2. 锁消除: JIT 编译器在运行时,通过逃逸分析如果发现某个锁对象不可能被其他线程访问到(即不会发生竞争),它会将这个锁操作完全消除掉。
  3. 锁粗化: 如果 JVM 检测到有一连串连续的操作都对同一个对象反复加锁和解锁(即使是在循环中),它可能会将加锁的范围扩大(粗化)到整个操作序列的外部,从而减少不必要的锁申请/释放次数。
  4. 偏向锁的争议与默认关闭: 在现代多核、高并发环境下,共享数据的竞争是常态,偏向锁在首次获得和撤销时的开销,以及它对应用程序启动性能的影响(大量类初始化时偏向锁操作)变得不可忽视。自 JDK 15 起,偏向锁默认被禁用,轻量级锁成为更常见的起点。-XX:-UseBiasedLocking 可以显式关闭它(在 JDK 15+ 已经是默认行为)。
  5. hashCode() 的影响: 当调用一个未被覆盖的 Object.hashCode()System.identityHashCode() 时,如果对象处于偏向锁或轻量级锁状态,会导致锁撤销或升级,因为无锁状态下的 Mark Word 需要存储哈希码。

理解锁升级的意义

锁升级机制体现了 JVM 在性能正确性之间所做的精妙平衡:

  • 无竞争/低竞争: 最大程度减少开销(偏向锁、轻量级锁)。
  • 高竞争: 保证正确性和线程调度的公平性/效率,接受较大的开销(重量级锁)。

了解锁升级机制对于编写高效、正确的并发程序至关重要。它解释了为什么简单的 synchronized 在低竞争场景下性能可以非常好,而在高竞争场景下性能会显著下降。在需要极高并发性能的场景下,开发者可能会选择更灵活、可优化的显式锁(如 ReentrantLock),但 synchronized 结合锁升级在大多数场景下已经是非常高效且简洁的选择。

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

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

相关文章

金蝶云星空 (9.0版本) ERP的WebApi接口随机出现SSLException

环境: java-1.8.0-openjdk-1.8.0.131 hutool-all 依赖, 5.8.25版本 项目背景: 发版上线,用的hutool工具类 HttpUtil.createPost() ,请求域名为https://xxx.ik3cloud.com/k3cloud 的金蝶ERP webapi接口 问题&#xff1…

用java,把12.25.pdf从最后一个点分割,得到pdf

要在Java中从文件名 12.25.pdf 的最后一个点(.)分割文件名和扩展名,可以使用 String 类的 lastIndexOf() 和 substring() 方法。以下是一个示例代码: public class FileNameSplitter {public static void main(String[] args) {St…

UE5 重新编译插件版本

打开要转换的UE的安装目录,一直找到这个文件 不要双击,在地址栏里输入cmd打开命令行,输入如下指令 RunUAT.bat BuildPlugin -plugin"E:\OldPlugin\chatbot5.3\chatbot\chatbot.uplugin" -package"E:\NewPlugin"-plugin…

Linux下的调试器-gdb(16)

文章目录 预备知识(9-2.30.00)快速认识 gdbgdb 的命令1. 更换成 cgdb2. 打和去除断点3. 逐语句与逐过程4. 使能(激活)断点 调试思想1. 找到问题(找到问题所在的区域)2. 查看代码的上下文 补充调试技巧1. wa…

李宏毅NLP-7-计算分数和训练和测试

文章目录 分数计算训练测试 分数计算 插入式序列生成模型的概率计算逻辑,核心是将 “生成序列 h 的过程” 拆解为一系列插入操作,并通过步骤概率的乘积计算总概率 P ( h ∣ X ) P(h∣X) P(h∣X)。以下从 模型框架、步骤分解、概率计算 三个层面解析&…

Python字符与ASCII转换方法

在Python中,可以使用内置函数 ord() 和 chr() 来转换字符和ASCII码: ​获取字符的ASCII码​ - 用 ord() ascii_code ord(A) # 返回 65 ​将ASCII码转为字符​ - 用 chr() character chr(65) # 返回 A 示例: # 打印字母A-Z的ASCII码…

[IMX][UBoot] 10.启动流程 (6) - bootz 命令启动 Linux

文章链接 UBoot 启动流程 (1) - 基本流程 UBoot 启动流程 (2) - 平台前期初始化阶段 - board_init_f UBoot 启动流程 (3) - UBoot 程序重定位 - relocate_code UBoot 启动流程 (4) - 平台后期初始化阶段 - board_init_r UBoot 启动流程 (5) - UBoot 运行阶段 - main_loop …

TCP 三次握手协商 MSS 前,如何确定 MSS 值(结合 Linux 内核源码分析)

文章目录 一、SYN总结影响 SYN MSS 的因素 二、SYNACK总结影响 SYNACK MSS 的因素 结合 Linux 内核源码 一、SYN 总结影响 SYN MSS 的因素 套接字选项 TCP_MAXSEG路由选项 advmss出口 MTU 减去 40(TCP 和 IP 的固定首部大小)IPV4_MAX_PMTU - 40(同上) 二、SYNACK 总结影响 SY…

面试150 矩阵置0

思路 我们使用两个标记集合,分别记录当矩阵的元素为0的时候的横、纵坐标。然后在对矩阵元素进行遍历,如果所在行或者所在列的索引在集合中,对应的矩阵元素修改为0即可 class Solution:def setZeroes(self, matrix: List[List[int]]) -> N…

Element UI 完整使用实战示例

以下是 Element UI 的完整使用实战示例,涵盖从环境搭建、基础组件使用到项目实战的全流程,结合多个实际场景和代码示例: 一、环境搭建与基础配置 1. 安装 Element UI 通过 npm 或 yarn 安装: npm install element-ui --save # …

C# 线程同步(一)同步概念介绍

目录 1.阻塞(Blocking) 2.阻塞 VS 轮询 3.线程状态 到目前为止,我们已经阐述了如何在线程上启动任务、配置线程以及实现双向数据传递。同时,我们也说明了局部变量是线程私有的,而引用可以通过共享字段在线程间传递以…

解决leetcode第3588题.找到最大三角形面积

3588.找到最大三角形面积难度:中等问题描述:给你一个二维数组coords,大小为nx2,表示一个无限笛卡尔平面上n个点的坐标。找出一个最大三角形的两倍面积,其中三角形的三个顶点来自coords中的任意三个点,并且该…

WIFI 安全测试记录

之前为实训课特意买的无线网卡没用上,但是我怎么可能让他荒废。所以用了几个下午,浅学了WiFi,当然没找到什么好教材,自己摸索着学的很基础,主要是当练习了,特此把我此前学习…WiFi密码实践过程写上来。 省流…

android14设置--网络--Internet副标题修改

收银机订制项目 插SIM卡,设备使用数据流量时,设置–网络–Internet副标题显示对应SIM卡运营商名称,客户要求修改这时的名称(注意图标也要同步修改) packages\apps\Settings\src\com\android\settings\network\InternetPreferenceController.j…

Web3区块链有哪些岗位?

Web3区块链领域的岗位丰富多样,涵盖技术开发、产品管理、运营、商务等多个方面,以下是具体介绍: - 技术开发类: - 智能合约开发工程师:负责编写、审计和优化智能合约,常见于DeFi开发,包括抵押…

解决 Spring Boot 对 Elasticsearch 字段没有小驼峰映射的问题

场景重现在使用 MyBatis/Mybatis-Plus 框架对 MySQL 操作时习惯了字段名小驼峰映射,然而在操作 Elasticsearch 时发现字段名没有小驼峰映射。解决方法1. 使用 ObjectMapper 手动转换: 这是最直接也最常用的方法。 在 Spring Boot 应用中使用 ObjectMappe…

Error:Cannot find module ‘chokidar‘

错误复现 在vue开发中,出现报错:Error:Cannot find module ‘chokidar’ 原因 缺包导致 解决方案 直接安装依赖包 npm install chokidar依旧无效,删除node_modules重新安装 rm -rf node_modules npm i

Spring AI 向量数据库详解与 RAG 简单实战项目

一、什么是向量数据库? 向量数据库用于存储、检索稠密语义向量(Embedding),是构建 RAG(检索增强生成)系统的核心组件。它支持近似最近邻搜索(ANN),可根据语义相似度找出…

【RK3568+PG2L50H开发板实验例程】Linux部分/FPGA FSPI 通信案例

本原创文章由深圳市小眼睛科技有限公司创作,版权归本公司所有,如需转载,需授权并注明出处(www.meyesemi.com) 1. 简介 本案例旨在 ARM端运行 Linux系统,基通过 FSPI测试。 2. ARM端和 FPGA端通信流程 (1)ARM端实现SP…

github如何创建一个自己的仓库保姆级教程

文章目录 准备阶段(github官网)添加ssh公钥添加token创建仓库 本地设置本地代理创建仓库添加文件到仓库进行提交 准备阶段(github官网) 添加ssh公钥 创建SSH KEY。先看一下你C盘用户目录下有没有.ssh目录,有的话看下里面有没有id_rsa和id_rsa.pub这两个文件&#…