news 2026/4/16 18:33:18

锁的进化史:从偏向锁到重量级锁的奇幻之旅

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
锁的进化史:从偏向锁到重量级锁的奇幻之旅

文章目录

    • 为什么需要这么多锁?
    • 锁的“状态机”:四种锁状态
    • 偏向锁:专一的锁
      • 为什么需要偏向锁?
      • 偏向锁的工作原理
      • 偏向锁的撤销
    • 轻量级锁:温和的竞争
      • 为什么需要轻量级锁?
      • 轻量级锁的工作原理
      • 自旋优化:耐心等待的策略
    • 重量级锁:真正的强者
      • 重量级锁的实现
      • 重量级锁的工作流程
    • 锁升级的全过程
    • 实战场景分析
      • 场景一:单线程环境(适合偏向锁)
      • 场景二:低竞争环境(适合轻量级锁)
      • 场景三:高竞争环境(需要重量级锁)
    • 锁优化的其他技术
      • 锁粗化(Lock Coarsening)
      • 锁消除(Lock Elimination)
    • 如何选择合适的锁策略?
    • 总结
    • 参考文章

大家好,我是你们的技术老友科威舟,今天,我们要一起探索Synchronized锁的升级之路——从偏向锁、轻量级锁到重量级锁的奇幻之旅。如果你曾对Java并发编程感到头疼,那么这篇文章就是你的布洛芬!

深入理解Java并发编程的锁优化,让你的程序性能飞起来!

为什么需要这么多锁?

在开始之前,我们先思考一个简单的问题:为什么Java不直接用最强大的重量级锁,而是要搞这么多锁状态?

想象一下,你去一家快餐店点餐。

重量级锁就像在收银台前修了一个小房间,每次只能进入一个人点餐,其他人必须在外面排队等待。这安全吗?绝对安全!但效率呢?堪忧!

而现实中,大多数情况是:餐厅里其实没什么顾客(没有竞争),或者即使有多个顾客,也是轮流点餐(交替执行),而不是同时挤在收银台前。

JDK的开发者们也意识到了这个问题,于是在JDK 1.6中,对synchronized进行了大幅优化,引入了我们今天要讲的锁升级机制

锁的“状态机”:四种锁状态

Java中的锁有四种状态,它们的关系如下所示:

锁状态标志位特点
无锁01对象未锁定
偏向锁01优化同一线程重复获取锁的场景
轻量级锁00优化多个线程交替执行同步块的场景
重量级锁10真正的互斥锁,适用于高竞争场景

锁只能从低到高升级,不能降级(虽然有极少数特殊情况,但一般认为不可降级)。

偏向锁:专一的锁

为什么需要偏向锁?

HotSpot的作者发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。比如,你在一个线程安全的多步操作中,可能会多次进入同一个同步块:

publicclassSafeCounter{privateintcount=0;publicvoidsafeIncrement(){synchronized(this){count++;// 其他操作...synchronized(this){// 再次进入同步块count++;}}}}

如果没有偏向锁,每次进入同步块都需要执行CAS操作,而CAS虽然比重量级锁高效,但仍有开销。

偏向锁的工作原理

偏向锁的核心理念是:如果锁始终由同一个线程使用,就不要反复加锁解锁了

加锁过程

  1. 检查对象头的Mark Word,判断是否处于可偏向状态(标志位为01,是否偏向为0)
  2. 如果是可偏向状态,通过CAS操作将当前线程ID记录到Mark Word中
  3. 如果CAS成功,该线程以后每次进入这个同步块,都不需要任何同步操作

举个例子:偏向锁就像是你家的门锁。只有你家人(同一线程)有钥匙,每次回家直接开门就行,不需要每次都在门口检查身份证。

偏向锁的撤销

当有另一个线程尝试获取偏向锁时,偏向锁就要被撤销了。这个过程需要等到全局安全点(在这个时间点上没有正在执行的字节码),然后检查原持有偏向锁的线程是否还存活。

  • 如果原线程已不存活或不在同步块中:将对象设置为无锁状态,然后新线程可以重新偏向或升级为轻量级锁
  • 如果原线程还在同步块中:升级为轻量级锁

偏向锁的适用场景:只有一个线程访问同步块,且不存在竞争的情况。在高并发场景下,偏向锁反而会降低性能(因为多了撤销操作),此时可以通过-XX:-UseBiasedLocking禁用。

轻量级锁:温和的竞争

为什么需要轻量级锁?

当偏向锁遇到竞争时,就会升级为轻量级锁。轻量级锁适应的场景是线程交替执行同步块,而不是真正的同时竞争。

想象一下公司卫生间的使用情况:多个人会使用,但通常是轮流使用,而不是同时挤在门口争夺使用权。

轻量级锁的工作原理

加锁过程

  1. 在代码进入同步块时,如果同步对象处于无锁状态,JVM会在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间
  2. 将对象头的Mark Word复制到锁记录中(称为Displaced Mark Word)
  3. 使用CAS操作尝试将对象的Mark Word更新为指向锁记录的指针
  4. 如果CAS成功,当前线程获得锁;如果失败,表示存在竞争,尝试自旋获取锁

轻量级锁的释放

  1. 使用CAS操作将Displaced Mark Word替换回对象头
  2. 如果成功,同步完成;如果失败,表示锁已膨胀,需要在释放锁的同时唤醒被挂起的线程

自旋优化:耐心等待的策略

轻量级锁在竞争失败后,不会立即升级为重量级锁,而是会进行自旋等待

自旋可以理解为:“我再等一会儿,说不定马上就能拿到锁了”。

// 自旋的简单理解for(inti=0;i<MAX_SPIN_TIMES;i++){if(tryGetLock()){// 获取锁成功!return;}// 稍微等待一下再尝试shortWait();}// 自旋多次还没拿到锁,升级为重量级锁upgradeToHeavyweightLock();

JDK 1.6引入了适应性自旋,意味着自旋时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者状态决定。

重量级锁:真正的强者

当轻量级锁自旋超过一定次数(或一个线程持有锁,另一个在自旋,又有第三个来访时),轻量级锁会升级为重量级锁。

重量级锁的实现

重量级锁依赖于操作系统的mutex锁实现,线程的阻塞和唤醒需要从用户态切换到内核态,成本很高。

重量级锁使用ObjectMonitor实现,其主要结构包括:

  • ContentionList:竞争队列,所有请求锁的线程首先被放在这个队列中
  • EntryList:候选队列,ContentionList中有资格成为候选资源的线程被移动到这里
  • WaitSet:等待集合,调用wait()方法的线程被放置在这里
  • Owner:当前持有锁的线程

重量级锁的工作流程

  1. 线程尝试获取锁,如果成功,成为Owner
  2. 如果失败,线程被封装成ObjectWaiter对象,加入到ContentionList中
  3. 当持有锁的线程释放锁时,会根据特定策略从ContentionList或EntryList中选取一个线程唤醒

重量级锁就像医院的专家号:每个人必须严格排队,即使医生暂时闲着,也得按规矩来。公平,但效率可能不高。

锁升级的全过程

现在我们把整个锁升级过程串联起来:

  1. 初始状态:对象被创建后,处于可偏向状态但未偏向任何线程(匿名偏向)
  2. 第一次加锁:线程A首次进入同步块,使用CAS将线程ID设置到对象头,进入偏向锁状态
  3. 同一线程重入:线程A再次进入同步块,检查对象头中的线程ID与自己一致,直接通过,无需同步操作
  4. 出现竞争:线程B尝试获取锁,发现锁已被线程A偏向
  5. 偏向锁撤销:等待全局安全点,检查线程A状态
  6. 升级轻量级锁:如果线程A仍需要锁,升级为轻量级锁,线程A成为锁持有者,线程B自旋等待
  7. 自旋过度:如果线程B自旋等待时间过长,或又有线程C来竞争锁
  8. 升级重量级锁:轻量级锁升级为重量级锁,线程B和C进入阻塞状态

实战场景分析

场景一:单线程环境(适合偏向锁)

publicclassSingleThreadScenario{publicvoidprocess(){List<String>data=fetchData();synchronized(this){// 处理数据processData(data);}// 其他操作...synchronized(this){// 再次处理furtherProcess(data);}}}

这种情况下,偏向锁可以大幅提升性能,因为同一线程多次获取锁时几乎零开销。

场景二:低竞争环境(适合轻量级锁)

publicclassLowContentionScenario{publicvoidprocess(){ExecutorServiceexecutor=Executors.newFixedThreadPool(2);// 两个线程交替执行,不是同时竞争for(inti=0;i<10;i++){executor.submit(()->{synchronized(this){// 短暂的同步操作shortOperation();}});}}}

这种情况下,轻量级锁通过自旋避免线程阻塞,提高响应速度。

场景三:高竞争环境(需要重量级锁)

publicclassHighContentionScenario{privatefinalObjectlock=newObject();publicvoidhighContentionMethod(){ExecutorServiceexecutor=Executors.newFixedThreadPool(10);// 10个线程激烈竞争同一把锁for(inti=0;i<100;i++){executor.submit(()->{synchronized(lock){// 较长的同步操作longRunningOperation();}});}}}

这种情况下,轻量级锁会导致大量自旋消耗CPU,重量级锁虽然阻塞线程,但总体效率更高。

锁优化的其他技术

除了锁升级,JVM还提供了其他锁优化技术:

锁粗化(Lock Coarsening)

将多个连续的锁操作合并为一个更大范围的锁操作。

// 锁粗化前publicvoidappend(){stringBuffer.append("a");stringBuffer.append("b");stringBuffer.append("c");}// 锁粗化后(JVM自动优化)publicvoidappend(){// 将三次加锁解锁合并为一次synchronized(stringBuffer){stringBuffer.append("a");stringBuffer.append("b");stringBuffer.append("c");}}

锁消除(Lock Elimination)

JVM通过逃逸分析技术,发现某些锁操作不可能被其他线程访问,就会将这些锁操作消除。

publicStringcreateString(){// stringBuffer是局部变量,不可能被其他线程访问StringBufferstringBuffer=newStringBuffer();stringBuffer.append("hello");stringBuffer.append("world");returnstringBuffer.toString();}

这种情况下,JVM会消除StringBuffer内部的同步操作。

如何选择合适的锁策略?

  1. 如果确定是单线程环境:可以开启偏向锁(默认开启)
  2. 如果是低竞争环境:轻量级锁是最佳选择
  3. 如果是高竞争环境:考虑禁用偏向锁和自旋锁,直接使用重量级锁
  4. 极端高并发场景:考虑使用Java并发包中的ReentrantLock等更高级的锁机制

可以通过以下JVM参数进行调优:

  • 关闭偏向锁:-XX:-UseBiasedLocking
  • 关闭自旋锁:-XX:-UseSpinning
  • 批量重偏向阈值:-XX:BiasedLockingBulkRebiasThreshold=20

总结

Java的锁升级机制是一个精美的性能优化方案,它体现了按需分配的思想:根据实际的竞争情况,提供不同级别的锁机制。

偏向锁适用于单线程重复访问的场景,轻量级锁适用于低竞争交替执行的场景,重量级锁适用于高竞争的场景。理解这些锁的工作原理和升级过程,有助于我们编写更高效的并发程序,并在出现性能问题时能准确诊断。

记住,没有绝对的优劣,只有适合的场景。选择合适的锁策略,让你的程序在并发世界中游刃有余!

参考文章

  1. https://blog.51cto.com/universsky/5377002
  2. https://blog.csdn.net/chengyan_1992/article/details/124803701
  3. https://blog.csdn.net/w1475995549/article/details/139992087
  4. https://blog.csdn.net/lp284558195/article/details/115547269
  5. https://blog.csdn.net/MariaOzawa/article/details/107665689

希望这篇文章能帮助你理解Java锁升级机制。如果有任何问题,欢迎在评论区留言讨论!下次我们将深入探讨Java并发包中的其他高级特性,敬请期待!

更多技术干货欢迎关注微信公众号科威舟的AI笔记~

【转载须知】:转载请注明原文出处及作者信息

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 16:23:04

【前端知识点总结】关于基地址baseURL的介绍

在前端开发的日常工作中&#xff0c;我们无时无刻不在与各种 URL 打交道&#xff1a;请求后端 API、加载静态资源、进行页面跳转。当项目规模扩大&#xff0c;环境变得复杂&#xff08;开发、测试、生产&#xff09;&#xff0c;硬编码的 URL 很快就会变成一场维护噩梦。 这时…

作者头像 李华
网站建设 2026/4/16 12:28:21

Flink SQL EXPLAIN “看懂计划”到“用 PLAN_ADVICE 调优”

1. EXPLAIN 能解决什么问题&#xff1f; 在 Flink Table/SQL 里&#xff0c;EXPLAIN 主要用来&#xff1a; 看清楚&#xff1a;SQL 会被解析成什么 逻辑计划&#xff08;Logical Plan / AST&#xff09;看明白&#xff1a;优化器做了哪些 算子改写&#xff08;如 Filter 下推、…

作者头像 李华
网站建设 2026/4/16 9:23:11

一生一芯学习:程序,运行时环境与AM(一)

前我们已经跑通了cpu-test和实现了riscv-I型指令所需的42条指令&#xff0c;现在我们已经可以到跑简单程序的地步了&#xff0c;我们也希望运行简单的程序&#xff0c;因此我们需要运行时环境(runtime environment)。比如现在要结束程序&#xff0c;那我们就要用提前准备好的AP…

作者头像 李华
网站建设 2026/4/15 18:26:13

DAY27 pipeline管道

一、知识回顾&#xff1a; 1、转化器和估计器的概念 &#xff08;1&#xff09;转换器 转化器通俗的来说就是对数据进行预处理的工具&#xff0c;转换器的特点是无状态的&#xff0c;即它们不会存储任何关于数据的状态信息&#xff08;指的是不存储内参&#xff09;&#xff0c…

作者头像 李华
网站建设 2026/4/16 3:29:32

Colima 下 docker pull 失败自查流程

macOS Colima 下 docker pull 超时问题排查总结 一、问题现象 在 macOS 上使用 colima 运行 Docker service 时&#xff0c;执行&#xff1a; docker pull BALABALA报错&#xff1a; Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http…

作者头像 李华
网站建设 2026/4/16 9:23:15

基于springboot的健身房预约与学习管理系统

博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;熟悉各种主流语言&#xff0c;精通java、python、php、爬虫、web开发&#xff0c;已经做了多年的设计程序开发&#xff0c;开发过上千套设计程序&#xff0c;没有什么华丽的语言&#xff0c;只有实…

作者头像 李华