本文旨在为有经验的Java工程师彻底厘清`synchronized`关键字背后的锁升级机制。我们将深入探讨偏向锁、轻量级锁与重量级锁的设计哲学、实现细节与性能权衡。你将理解这并非一个孤立的JVM特性,而是操作系统、CPU硬件与编译器协同优化的经典范例。本文不满足于表面概念,而是通过剖析对象头(Mark Word)、OS线程调度、CPU缓存一致性等底层原理,为你构建一个完整、深刻的知识体系,以应对高并发场景下的真实性能挑战。
现象与问题背景
在许多高并发系统的性能瓶颈分析中,`synchronized`的身影时常出现。一个常见的误解是将其简单地标签为“重量级”、“性能差”。然而,在现代HotSpot虚拟机中,`synchronized`的实现远比这复杂。例如,在一个典型的电商秒杀场景,库存扣减逻辑通常会被`synchronized`保护。在秒杀开始的瞬间,线程竞争急剧增加,我们可能会观察到系统CPU的上下文切换(Context Switches)次数飙升,导致TPS(Transactions Per Second)远低于预期。但同样的代码,在预热阶段或低负载时,性能开销却微乎其微。为什么同一个`synchronized`关键字在不同竞争强度下表现出如此迥异的性能特征?这背后的秘密,就是JVM的锁升级机制。它试图在“无锁的性能”和“有锁的正确性”之间找到一个动态的平衡点,但这套机制的有效性,依赖于我们对它的深刻理解和对业务场景的正确判断。
关键原理拆解
要理解Java锁,我们必须回归计算机科学的基础。锁的本质是为了解决“互斥(Mutual Exclusion)”问题,确保在多线程环境下,对共享资源的访问是原子性的。这背后依赖于更底层的硬件和操作系统支持。
- CPU原子指令: 现代CPU提供了一系列原子指令,这是实现一切并发原语的基石。其中最著名的是CAS(Compare-And-Swap)。CAS指令包含三个操作数:内存位置V、预期原值A和新值B。当且仅当V处的值等于A时,CPU才会原子性地将V处的值更新为B,并返回操作是否成功。这个过程是硬件保证的原子性,不会被中断。Java中`Unsafe`类和`java.util.concurrent.atomic`包下的类,其核心就是CAS。轻量级锁的实现严重依赖此指令。
- 操作系统内核原语: 当线程竞争激烈,无法通过简单的自旋(spin)等待锁释放时(这会空耗CPU),就需要将线程挂起,让出CPU。这个过程必须由操作系统内核(Kernel)完成。线程从用户态(User Mode)陷入内核态(Kernel Mode),调用如Linux下的`futex`(Fast Userspace Mutex)或Windows的`event`对象等机制。内核会将该线程放入一个等待队列,并触发一次上下文切换(Context Switch),调度其它就绪线程运行。当锁被释放时,内核再从等待队列中唤醒一个线程。这个“陷入内核 -> 上下文切换 -> 唤醒线程”的路径非常长,涉及大量CPU周期,这就是“重量级锁”之所以“重”的根本原因。
- Java对象头(Mark Word): JVM的锁信息并非存放在一个全局的锁管理器中,而是精巧地存储在每个Java对象自身的头部。对象头在64位JVM中通常占128位(16字节),其中一部分被称为“Mark Word”(64位/8字节),它就是锁状态机的大本营。Mark Word的位布局会根据对象的锁状态(无锁、偏向锁、轻量级锁、重量级锁)以及GC状态动态变化,存储如锁标志位、持有锁的线程ID、锁记录指针、哈希码、GC分代年龄等信息。理解Mark Word的结构是理解锁升级过程的钥匙。
锁状态机:Mark Word的舞蹈
Java对象头的Mark Word在不同锁状态下,其内部比特位的含义是不同的。以下是64位JVM中一个简化的状态图:
- 无锁状态 (001): Mark Word的低3位为`001`。高54位可以用来存储对象的`identityHashCode`。
- 偏向锁状态 (101): Mark Word的低3位为`101`。这时,大部分位(约54位)用来存储持有该偏向锁的线程ID。还有一个纪元(epoch)字段用于处理偏向锁的批量撤销。
- 轻量级锁状态 (00): Mark Word的低2位为`00`。此时,高62位是一个指针,指向持有锁的线程栈帧中的一个锁记录(Lock Record)。线程通过CAS操作尝试将Mark Word更新为指向自己栈上锁记录的指针。
- 重量级锁状态 (10): Mark Word的低2位为`10`。此时,高62位是一个指针,指向一个与该对象关联的`ObjectMonitor`对象。所有关于锁的等待、唤醒等重量级操作都委托给了这个C++实现的`ObjectMonitor`。
- GC标记状态 (11): 在GC期间,Mark Word用于标记对象。
–
–
–
–
整个锁升级过程,本质上就是根据线程竞争的激烈程度,修改Mark Word的标志位,并改变其指针或数据的指向,从一个低成本的状态迁移到高成本但适应性更强的状态。
核心模块设计与实现:锁升级的三部曲
接下来,我们以一个极客工程师的视角,深入到`synchronized(obj)`这行代码背后发生的具体流程。
第一幕:偏向锁 (Biased Locking)
哲学: 统计学表明,在绝大多数情况下,一个锁在整个生命周期中只被同一个线程获取。基于这个乐观假设,偏向锁的设计目标是:当一个线程获得了锁之后,它在之后再次进入该同步块时,不需要再进行任何CAS或同步操作,从而达到近乎无锁的性能。
实现细节:
- 线程A首次进入同步块,发现对象`obj`的Mark Word处于无锁或可偏向状态。
- JVM通过一次CAS操作,尝试将线程A的ID写入`obj`的Mark Word,并将锁标志位置为`101`(偏向锁)。
- 如果CAS成功,线程A持有偏向锁。当它下一次再进入这个同步块时,只需检查Mark Word中的线程ID是否是自己的ID。如果是,则无需任何额外操作,直接进入临界区。这是一个极快的路径,连CAS都省了。
锁撤销(Revocation)—— 偏向锁的“坑”:
美好的假设总会被现实打破。当线程B尝试获取这个已被线程A持有的偏向锁时,偏向模式宣告失败。此时必须暂停持有偏向锁的线程A(通过一个全局安全点 – Safepoint),检查线程A是否还在同步块内。
- 如果线程A已退出同步块,那么很简单,将`obj`的Mark Word恢复为无锁状态或重新偏向给线程B。
- 如果线程A仍在同步块内,情况就复杂了,必须进行锁升级。偏向锁被撤销,升级为轻量级锁。线程A的锁状态被更新,然后线程B开始以轻量级锁的方式竞争。
这个撤销过程的开销是比较大的,需要一个全局安全点,这会暂停所有用户线程(Stop-The-World)。如果一个对象的锁在多个线程间频繁传递,偏向锁会因不断的撤销和重偏向,反而成为性能负担。这就是为什么在JDK 15之后,偏向锁被默认关闭的原因。
// HotSpot虚拟机C++伪代码 - 偏向锁快速路径检查
markOop mark = obj->mark();
if (mark->is_biased_lock() && mark->bias_to_thread() == Thread::current()) {
// 命中偏向锁,直接返回,无任何同步开销
return;
}
// 慢速路径,尝试加锁或升级
...
第二幕:轻量级锁 (Lightweight Locking)
哲学: 当偏向锁失败后,JVM并不会立即“投降”并切换到重量级锁。它做了第二个乐观假设:线程之间的锁竞争通常是短暂的,一个线程持有锁的时间不长,另一个线程稍微等一等(自旋)就能拿到锁,从而避免陷入内核态的巨大开销。
实现细节:
- 锁撤销偏向锁后,或一个无锁对象首次被两个线程同时竞争时,进入轻量级锁路径。
- 竞争线程(比如线程B)会在自己的线程栈上创建一个名为“锁记录”(Lock Record)的空间,用于存放`obj`当前Mark Word的一个拷贝(Displaced Mark Word)。
- 然后,线程B使用CAS指令,尝试将`obj`的Mark Word原子性地更新为指向这个锁记录的指针。
- 如果CAS成功,线程B获得轻量级锁,Mark Word的锁标志位变为`00`。
- 如果CAS失败,说明锁已被其他线程(比如线程A)持有。线程B不会立即挂起,而是会进行“自旋”。它会在一个小的循环里不断地尝试执行CAS操作,寄希望于线程A能很快释放锁。
// HotSpot虚拟机C++伪代码 - 轻量级锁加锁
void* lock_record_ptr = &thread->stack.lock_record;
markOop current_mark = obj->mark();
// CAS: 尝试将对象的Mark Word指向线程栈上的Lock Record
if (cas(obj->mark_addr(), current_mark, lock_record_ptr) == current_mark) {
// 成功获取轻量级锁
// 将原Mark Word存入Lock Record
lock_record->set_displaced_header(current_mark);
} else {
// CAS失败,进入自旋或膨胀流程
inflate_lock(obj);
}
JVM还引入了自适应自旋(Adaptive Spinning)。如果一个线程在某个锁上自旋成功过,那么下次它可能会被允许自旋更长的时间;反之,如果自旋很少成功,那么后续可能会跳过自旋直接膨胀。这是JVM在性能和CPU消耗之间做的动态权衡。
第三幕:重量级锁 (Heavyweight Locking)
哲学: 当自旋也无法解决问题时(竞争激烈,或锁持有时间长),就必须放弃乐观,承认这是一个高竞争场景。此时,继续自旋空耗CPU已不再明智,应当让出CPU,进入操作系统层面的等待队列。
实现细节:
- 当一个线程自旋超过一定次数(或自适应判断后),轻量级锁就会“膨胀”(Inflate)为重量级锁。
- 这个过程首先会为对象`obj`关联一个C++实现的`ObjectMonitor`对象。
- 然后,`obj`的Mark Word会被修改,锁标志位置为`10`,并存储一个指向这个`ObjectMonitor`的指针。
- 此时,所有后续尝试获取该锁的线程,都会在`ObjectMonitor`的入口队列(`_EntryList`)中排队,并通过操作系统原语(如`pthread_mutex_lock`或`futex`)被挂起,进入阻塞状态,等待被唤醒。
- 当持有锁的线程释放锁时,它会唤醒`_EntryList`中的一个或多个等待线程,这些线程将重新参与锁的竞争。
一旦升级为重量级锁,通常就不会再降级回轻量级锁或偏向锁了(尽管在某些极特殊情况下,JVM理论上可以实现锁的降级,但实践中很少发生)。这个状态的切换是昂贵的,但一旦进入,它就能很好地处理高并发、长时间的锁竞争场景。
性能优化与高可用设计
理解了锁升级,我们就能在工程实践中做出更明智的决策。
- 锁粒度控制: 这是最经典也是最有效的优化手段。如果`synchronized`保护的代码块过大,或者锁住了一个过于宽泛的对象(如`this`或一个全局集合),会导致锁持有时间变长,竞争加剧,更容易升级到重量级锁。应该尽可能地让锁的粒度变小,只保护真正需要互斥访问的临界区。例如,`ConcurrentHashMap`的分段锁思想就是典型的减小锁粒度的实践。
- 减少锁持有时间: 在同步块内部,只做必要的操作。将可以并行的、耗时的操作(如I/O、复杂的计算)移出同步块。锁持有的时间越短,线程冲突的概率就越低,锁停留在轻量级阶段的可能性就越大。
- 读写分离与无锁化: 对于读多写少的场景,`synchronized`这种排他锁的性能并不理想。可以考虑使用`java.util.concurrent.locks.ReadWriteLock`,允许多个读线程同时访问。在更高要求的场景下,可以考虑使用无锁数据结构,如`Atomic`系列类,或`ConcurrentLinkedQueue`等,它们完全基于CAS操作,避免了锁的开销和线程挂起的风险。
- 警惕偏向锁的代价: 在某些场景,例如一个任务队列由线程池中的不同线程轮流处理,每个任务都会操作同一个锁。这种情况下,锁会在不同线程间频繁易主,导致偏向锁被反复撤销,性能反而下降。此时,通过JVM参数 `-XX:-UseBiasedLocking` 关闭偏向锁,让系统直接从轻量级锁开始,可能会获得更好的性能。
架构演进与落地路径
对于一个系统中的并发控制,其演进路径通常遵循以下策略:
- 阶段一:默认信任`synchronized`。 在项目初期或非核心瓶颈路径,直接使用`synchronized`。它是最简单、最不易出错的同步方式。让JVM的锁升级机制自动处理大部分情况。不要过早优化。
- 阶段二:性能分析与热点定位。 当系统出现性能问题时,通过JFR(Java Flight Recorder)、Arthas等工具分析线程栈,找到锁竞争的热点。确认是`synchronized`导致的上下文切换开销或CPU空转。
- 阶段三:代码级优化。 针对定位到的热点,首先尝试代码层面的优化,即减小锁粒度和减少锁持有时间。这是投入产出比最高的优化。
- 阶段四:升级并发工具。 如果代码级优化已到极限,但性能仍不满足要求,考虑使用J.U.C包中更高级的工具。例如,用`ReentrantLock`替代`synchronized`以获得可中断、可超时的锁,或使用`ReadWriteLock`优化读多写少的场景。
- 阶段五:终极无锁化。 在对延迟和吞吐量要求极为苛刻的场景,如交易撮合引擎、高频计数器等,可以考虑采用无锁编程范式。这需要对内存模型、CAS操作有极深的理解,通过`Atomic`类或自己实现基于CAS的算法来避免使用任何锁。这是最复杂但性能最高的方式,需要谨慎使用。
总之,Java的锁升级机制是一项了不起的工程成就,它在易用性和高性能之间取得了精妙的平衡。作为一名资深工程师,我们的任务不是去绕过它,而是去理解它、利用它,并在它达到极限时,知道我们工具箱里还有哪些更锋利的武器。
延伸阅读与相关资源
-
想系统性规划股票、期货、外汇或数字币等多资产的交易系统建设,可以参考我们的
交易系统整体解决方案。 -
如果你正在评估撮合引擎、风控系统、清结算、账户体系等模块的落地方式,可以浏览
产品与服务
中关于交易系统搭建与定制开发的介绍。 -
需要针对现有架构做评估、重构或从零规划,可以通过
联系我们
和架构顾问沟通细节,获取定制化的技术方案建议。