synchronized锁升级机制技术文档
1. 引言:JDK6 锁优化背景与锁升级核心思想

1.1 传统 synchronized 的性能问题
在 JDK 5 及之前的版本中,synchronized 是一个纯粹的重量级锁实现,其底层依赖操作系统的互斥量(Mutex)来实现线程同步。这种实现方式存在显著的性能问题:
•内核态切换开销:线程阻塞和唤醒需要从用户态切换到内核态,这个过程涉及线程上下文的保存与恢复,单次切换开销约为微秒级
•无竞争场景的浪费:在大多数实际应用场景中(约 90% 以上),锁实际上只被同一个线程多次获取,不存在真正的竞争,但重量级锁机制仍然会执行完整的加锁解锁流程
•响应性差:线程一旦阻塞就会进入挂起状态,无法响应中断,直到获取锁为止
1.2 JDK6 锁优化的革命性改进
高效并发是 JDK 6 最重要的改进之一。HotSpot 虚拟机开发团队在这个版本中投入大量资源,实现了一整套锁优化技术:
•适应性自旋(Adaptive Spinning)
•锁消除(Lock Elimination)
•锁粗化(Lock Coarsening)
•轻量级锁(Lightweight Locking)
•偏向锁(Biased Locking)
1.3 锁升级的核心思想
锁升级(Lock Escalation/Upgrade)机制的核心设计哲学是:“按需分配,分级适配“
•无竞争场景:使用偏向锁,仅需一次 CAS 设置,后续访问无需任何同步操作
•轻度竞争场景:使用轻量级锁,通过用户态 CAS 操作替代内核态互斥,避免线程阻塞
•重度竞争场景:升级为重量级锁,使用操作系统互斥量保证线程安全
锁升级是一个单向不可逆的过程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,只能升级不能降级。
2. 锁的载体:Java 对象头与 Mark Word 的动态结构
2.1 Java 对象内存布局
在 HotSpot 虚拟机中,Java 对象在内存中分为三部分:
|
┌────────────────────────────────┐ │Object Header│对象头(占8或16字节) ├────────────────────────────────┤ │Instance Data│实例数据 ├────────────────────────────────┤ │Padding│对齐填充(保证8字节对齐) └────────────────────────────────┘ |
对象头(Object Header)又包含两部分:
1.Mark Word(标记字段):存储对象自身的运行时数据,是锁实现的核心载体
2.Class Pointer(类型指针):指向对象所属类的元数据指针
2.2 Mark Word 的动态结构设计
Mark Word 是一个非固定的数据结构,在64 位JVM 中占用 8字节(64bit)。为了在极小的空间内存储尽可能多的信息,它采用了动态位复用技术 —— 根据对象当前的锁状态,各位段的含义会发生变化。
2.3 64 位 JVM 各锁状态的位分布
以下是 64 位 JVM 中 Mark Word 在不同锁状态下的精确位分布:
|
锁状态 |
63 ~ 10 位(54bit) |
9 ~ 8 位(2bit) |
7 位(1bit) |
6 ~ 3 位(4bit) |
2 位(1bit) |
1 ~ 0 位(2bit) |
|
无锁 |
unused:25 | hashcode:31 |
– |
unused:1 |
age:4 |
biased_lock: 0 |
lock: 01 |
|
偏向锁 |
thread:54 |
epoch:2 |
unused:1 |
age:4 |
biased_lock: 1 |
lock: 01 |
|
轻量级锁 |
ptr_to_lock_record:62 |
– |
– |
– |
– |
lock: 00 |
|
重量级锁 |
ptr_to_monitor:62 |
– |
– |
– |
– |
lock: 10 |
|
GC 标记 |
empty:62 |
– |
– |
– |
– |
lock: 11 |
关键位说明:
•lock(2bit):锁状态标志位,决定了Mark Word 的整体结构
○01:无锁或偏向锁(由biased_lock 区分)
○00:轻量级锁
○10:重量级锁
○11:GC标记
•biased_lock(1bit):偏向锁启用标志
○0:未启用偏向锁,处于无锁状态
○1:启用偏向锁
•age(4bit):对象GC 分代年龄,最大15
•thread(54bit):持有偏向锁的线程ID
•epoch(2bit):偏向锁的时间戳,用于批量撤销
3. 四大锁状态详解
3.1 无锁(No Lock)
状态特征
•标志位:biased_lock=0, lock=01
•Mark Word 内容:存储对象的identity hash code、GC分代年龄
•触发时机:对象刚创建时,或偏向锁被彻底撤销后
无锁状态的关键特性
1.延迟偏向机制:JVM 启动后 4 秒内默认不启用偏向锁(可通过-XX:BiasedLockingStartupDelay=0关闭延迟)
2.hash code 触发:当对象调用hashCode()或System.identityHashCode()后,会禁用该对象的偏向锁功能
3.无同步开销:完全没有任何锁相关的性能损耗
3.2 偏向锁(Biased Lock)
设计目标
针对 \\“锁总是被同一个线程多次获取“\\的场景(占绝大多数),消除甚至连 CAS 操作都不需要做。
偏向锁获取流程
1.检查状态:访问同步块时,检查 Mark Word 中的 biased_lock 是否为 1,lock 是否为 01
2.测试偏向:
○如果线程 ID 为空:通过 CAS 操作将当前线程 ID 写入 Mark Word
○如果 CAS 成功:偏向成功,后续进入退出同步块无需任何操作
○如果线程 ID 等于当前线程 ID:直接进入同步块,无需任何操作
3.存在竞争:如果线程 ID 不等于当前线程ID,触发偏向锁撤销
偏向锁撤销(Unbias)
偏向锁的撤销是一个重量级操作,需要在全局安全点(Safe Point)执行:
1.暂停拥有偏向锁的线程
2.检查线程状态:
○线程已死亡:将对象头重置为无锁状态,允许重新偏向
○线程仍存活:
▪遍历线程栈帧,检查是否仍在使用该锁
▪仍在使用:升级为轻量级锁,将 Displaced Mark Word 写入线程的 Lock Record
▪不再使用:重置为无锁状态,允许重新偏向
3.恢复线程执行
特殊机制:批量重偏向与批量撤销
•批量重偏向(Bulk Rebias):当一个类的对象偏向撤销次数达到 20 次时,JVM 会认为该类的偏向模式有问题,允许这些对象重新偏向到其他线程
•批量撤销(Bulk Revoke):当撤销次数达到 40 次时,JVM 会禁用该类所有对象的偏向功能,直接进入轻量级锁流程
3.3 轻量级锁(Lightweight Lock)
设计目标
当多个线程交替执行同步块时,使用用户态的CAS 操作代替内核态的互斥量,避免线程切换的开销。
轻量级锁获取流程
1.创建锁记录:线程在自己的栈帧中创建一个名为Lock Record(锁记录)的空间
2.复制 Mark Word:将对象头的Mark Word 复制到锁记录中,称为Displaced Mark Word
3.CAS替换:通过CAS 操作尝试将对象头的Mark Word 替换为指向当前线程锁记录的指针
4.结果判断:
○CAS成功:获取轻量级锁成功,将锁标志位设为00
○CAS失败:检查Mark Word 是否指向当前线程的栈帧
▪是:说明是锁重入,直接进入同步块
▪否:说明存在真正的竞争,自旋等待或升级为重量级锁
轻量级锁释放流程
1.CAS还原:使用CAS 操作将Displaced Mark Word 从锁记录复制回对象头
2.结果判断:
○CAS成功:释放锁成功
○CAS失败:说明期间有其他线程尝试获取锁,已经升级为重量级锁,需要唤醒等待的线程
轻量级锁的优缺点
优点:
•无竞争时性能优异,仅需几次 CAS 操作
•用户态操作,无需内核态切换
缺点:
•存在竞争时,CAS 自旋会消耗 CPU 资源
•自旋次数有限,过度自旋反而降低性能
3.4 重量级锁(Heavyweight Lock)
升级触发条件
当出现以下情况时,轻量级锁会升级为重量级锁:
1.自旋次数超过阈值:JDK6 后为自适应自旋,由前一次自旋结果决定
2.多个线程同时竞争:两个以上线程同时争抢同一把锁
3.线程调用 wait ()/notify ():等待 / 通知机制必须依赖重量级锁
底层 Monitor 实现
重量级锁的底层是ObjectMonitor对象,其核心结构如下:
|
cppObjectMonitor {_owner:当前持有锁的线程指针_count:锁重入计数_waiters:等待队列(调用wait的线程)_EntryList:入口队列(争抢锁的线程)_cxq:竞争队列recursions:重入次数 } |
线程竞争流程
1.尝试直接获取:线程先尝试 CAS 设置_owner,成功则直接获取锁
2.进入队列:获取失败,进入_EntryList 队列阻塞
3.线程挂起:操作系统层面将线程挂起(park),进入内核态阻塞
4.锁释放唤醒:锁释放时,从队列中唤醒一个线程继续竞争
4. 完整锁升级流程图解与单向升级规则
4.1 锁升级完整流程图
|
对象创建↓ [无锁状态] biased_lock=0, lock=01↓ 第一个线程进入同步块↓ CAS设置线程ID [偏向锁状态] biased_lock=1, lock=01↓ 第二个线程进入↓ 检测到线程ID不匹配↓ 安全点撤销偏向 [轻量级锁状态] lock=00↓ 创建Lock Record↓ CAS替换对象头↓ 竞争加剧,自旋失败 [重量级锁状态] lock=10↓ 关联ObjectMonitor↓ 线程进入队列阻塞↓ 内核态互斥 |
4.2 各状态转换条件详解
|
状态转换 |
触发条件 |
|
无锁 → 偏向锁 |
第一个线程进入同步块,CAS 设置线程 ID 成功 |
|
偏向锁 → 轻量级锁 |
其他线程竞争,偏向锁撤销且原线程仍存活 |
|
偏向锁 → 无锁 |
偏向锁撤销且原线程已死亡,或调用 hashCode () |
|
轻量级锁 → 重量级锁 |
自旋超过次数、多线程同时竞争、调用 wait ()/notify () |
|
任意状态 → 重量级锁 |
不可逆,一旦升级为重量级锁,该对象的锁永远停留在重量级 |
4.3 单向升级规则
核心规则:锁只能升级,不能降级
1.不可逆性:一旦锁从偏向锁升级为轻量级锁,就无法再回到偏向锁状态;一旦升级为重量级锁,就永远是重量级锁
2.状态守恒:对象的锁状态只会向更 “重” 的方向发展
3.性能权衡:这是一种典型的 “简单假设” 优化 —— 假设大多数锁不会遇到激烈竞争,一旦遇到就接受性能下降的代价
|
注意:JDK 15 及以后版本,偏向锁默认已被禁用并标记为废弃,因为现代应用中偏向锁的收益越来越小,而维护成本越来越高。 |
5. 其他锁优化:锁消除、锁粗化、自适应自旋
5.1 锁消除(Lock Elimination)
核心原理
JIT 即时编译器在运行时,对一些代码上要求同步,但检测到不可能存在共享数据竞争的锁进行消除。
逃逸分析基础
锁消除的依据是 \\逃逸分析(Escape Analysis)\\技术:
•如果一个对象不会逃逸出当前线程,那么这个对象的读写就不存在线程安全问题
•对这样的对象加锁是完全没有必要的
典型场景
|
javapublic String concat(String s1, String s2, String s3) {// StringBuffer的append是synchronized方法// 但sb对象不会逃逸出方法,JIT会消除这里的锁StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);sb.append(s3);return sb.toString(); } |
相关参数
•-XX:+DoEscapeAnalysis:开启逃逸分析(默认开启)
•-XX:+EliminateLocks:开启锁消除(默认开启)
5.2 锁粗化(Lock Coarsening)
核心原理
如果虚拟机检测到有一系列连续的操作都是对同一个对象反复加锁解锁,甚至加锁操作出现在循环体中,会将锁的同步范围扩展(粗化)到整个操作序列的外部。
优化前后对比
优化前(频繁加锁解锁):
|
javafor (int i = 0; i < 10000; i++) {synchronized (this) {count++;} } // 10000次加锁 + 10000次解锁 |
优化后(锁粗化):
|
javasynchronized (this) {for (int i = 0; i < 10000; i++) {count++;} } // 1次加锁 + 1次解锁 |
设计考量
•减少锁的获取 / 释放次数,降低总开销
•但会增加锁的持有时间,可能影响并发性
•JVM 会自动权衡,只在收益大于成本时进行粗化
5.3 自适应自旋(Adaptive Spinning)
传统自旋锁的问题
固定自旋次数(如 10 次)的缺点:
•次数太少:刚要成功就放弃,升级为重量级锁
•次数太多:CPU 空转浪费资源
自适应自旋的智能策略
JDK 6 引入的自适应自旋,自旋时间不再固定,而是由:
1.上一次自旋结果:如果同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,虚拟机会认为这次自旋也很可能成功,允许自旋更长时间
2.锁的当前状态:如果对于某个锁,自旋很少成功过,虚拟机会直接省略自旋过程,直接阻塞线程
相关参数
•-XX:+UseSpinning:开启自旋(JDK6 后默认开启)
•-XX:PreBlockSpin:自旋次数(JDK6 后由 JVM 自适应调整)
6. 总结:锁升级的设计哲学与实际应用价值
6.1 锁升级的设计哲学
1. 场景分层,按需适配
•90%场景:单线程反复获取锁 →偏向锁(零开销)
•9%场景:多线程交替执行 →轻量级锁(CAS开销)
•1%场景:激烈竞争 →重量级锁(内核互斥)
2. 乐观假设,悲观兜底
•乐观假设:大多数锁不存在激烈竞争
•悲观兜底:竞争出现时,有完整的降级(升级)机制保证正确性
3. 性能优先,安全保障
•无竞争时极致性能
•有竞争时安全第一
•正确性永远优先于性能
6.2 实际应用价值
对应用开发者
1.无需手动选择锁类型:synchronized 会自动适配场景,比 ReentrantLock 更智能
2.理解性能差异:知道为什么不同场景下 synchronized 性能差异巨大
3.避免反模式:
○不要在锁对象上调用 hashCode ()(会禁用偏向锁)
○避免循环内加锁(触发锁粗化反而降低并发)
○高竞争场景考虑显式锁
6.3 版本演进与未来趋势
•JDK 6:引入完整锁升级机制,synchronized性能大幅提升
•JDK 8:优化偏向锁实现,默认开启偏向锁
•JDK 15:偏向锁默认禁用,标记为废弃(JEP 374)
•未来方向:更轻量的并发原语、硬件级事务内存支持
6.4 核心启示
synchronized 锁升级机制是工程权衡艺术的典范:
•它不追求在所有场景下都最优,而是在大多数常见场景下做到极致
•它用复杂的底层实现,换取上层使用的简单性
•它证明了:好的性能优化,不是让开发者做选择,而是让系统自动做对选择
理解锁升级,不仅是理解 synchronized 的底层原理,更是理解现代虚拟机如何在 “易用性” 与 “高性能” 之间找到完美平衡。
夜雨聆风