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

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.lryc.cn/news/578265.html

相关文章:

  • AI编程实战:Cursor黑科技全解析
  • AlpineLinux安装docker
  • 提示技术系列——思维树
  • 使用Verilog设计模块输出中位数,尽可能较少资源使用
  • Python 数据分析与机器学习入门 (五):Matplotlib 数据可视化基础
  • python环境快速搭建
  • Clickhouse源码分析-TTL执行流程
  • 直播 APP 开发需要多少成本
  • (LeetCode 面试经典 150 题) 135. 分发糖果 (贪心)
  • 【Springai】 2指定模型的三种方式(Ollama)
  • 【SpringAI】3.结构化输出,初级版
  • Spring Boot + ONNX Runtime模型部署
  • springboot中多个定时任务(@Scheduled)如何互不影响
  • 大数据(4)-spark
  • Webpack优化详解
  • Unity性能优化-渲染模块(1)-CPU侧(2)-DrawCall优化(2)GPUInstancing
  • 浪潮和曙光服务器的ipmi配置教程
  • 图灵完备之路(数电学习三分钟)----开关与延迟线
  • Ubuntu更换Home目录所在硬盘的过程
  • Pyhton-EXCEL与Mysql数据对比
  • 从设计到开发一个小程序页面
  • 鸿蒙NEXT-鸿蒙三层架构搭建,嵌入HMRouter,实现便捷跳转,新手攻略。(2/3)
  • HTML之常用基础标签
  • JavaScript异步编程的五种方式
  • 力扣 hot100 Day30
  • Spring生态:云原生与AI的革新突破
  • 七天学会SpringCloud分布式微服务——06——Sentinel
  • 从零到一通过Web技术开发一个五子棋
  • CSDN博客大搬家(本地下载markdown合适和图片本地化)
  • Stable Diffusion 项目实战落地:从0到1 掌握ControlNet 第四篇 风格化字体大揭秘:从线稿到涂鸦,ControlNet让文字焕发新生