这是一个非常经典且高频的Java并发面试题,也是理解 synchronized 性能优化的核心。

在 JDK 1.6 之前,synchronized 是一个“重量级锁”,效率较低。为了减少获得锁和释放锁带来的性能消耗,JDK 1.6 引入了偏向锁轻量级锁

锁的状态总共有四种,级别从低到高依次是:
无锁状态 (No Lock) -> 偏向锁 (Biased Lock) -> 轻量级锁 (Lightweight Lock) -> 重量级锁 (Heavyweight Lock)

只能升级,不能降级(除了GC阶段),这种策略是为了提高获得锁和释放锁的效率。


一、 核心基础:对象头 (Mark Word)

要理解锁升级,必须先理解 Java 对象在内存中的布局,特别是 对象头(Object Header) 中的 Mark Word

Mark Word 是一个非固定的数据结构,用于存储对象的运行时数据(HashCode、GC分代年龄、锁状态标志、线程持有的锁等)。

在 64 位虚拟机中,Mark Word 的大概结构如下(简化版):

锁状态

25bit (未使用)

31bit (HashCode)

1bit (未使用)

4bit (分代年龄)

1bit (偏向锁标识)

2bit (锁标志位)

无锁

...

HashCode

0

age

0

01

偏向锁

ThreadID (54bit)

Epoch (2bit)

...

age

1

01

轻量级锁

指向栈中 Lock Record 的指针 (62bit)

...

...

...

...

00

重量级锁

指向互斥量 (Monitor) 的指针 (62bit)

...

...

...

...

10

注意:锁标志位和偏向锁标识共同决定了锁的状态。


二、 锁升级的详细过程

1. 偏向锁 (Biased Lock)

场景:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。

原理

  • 当一个线程访问同步块并获取锁时,会在对象头和栈帧的锁记录里存储锁偏向的线程ID
  • 以后该线程在进入和退出同步块时,不需要进行CAS操作来加锁和解锁,只需简单测试一下对象头的 Mark Word 里是否存储着指向当前线程的偏向锁。
  • 如果测试成功,表示线程已经获得了锁。

偏向锁的撤销

  • 偏向锁使用了一种等到竞争出现才释放锁的机制。只有当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
  • 需要等待全局安全点(Safepoint,在这个时间点上没有正在执行的字节码)。
  • 暂停拥有偏向锁的线程,检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,升级为轻量级锁

:JDK 15 之后,偏向锁默认是禁用的(Deprecated),因为在现代高并发应用中,维护偏向锁撤销的成本可能高于其带来的收益。

2. 轻量级锁 (Lightweight Lock)

场景:线程交替执行同步块(即没有激烈的并发竞争),或者竞争非常短暂。

原理 (CAS)

  • 加锁:线程在执行同步块之前,JVM 会在当前线程的栈帧中创建用于存储锁记录的空间(Lock Record),并将对象头中的 Mark Word 复制到锁记录中(Displaced Mark Word)。
  • 然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。
  • 如果成功,当前线程获得锁,标志位变为 00
  • 自旋 (Spinning):如果失败,表示有竞争。当前线程不会立刻阻塞(不放弃CPU),而是进行自旋(也就是写一个 while(true) 空循环),尝试看看持有锁的线程是否很快释放。如果自旋一定次数后(或者智能自旋)还没拿到锁,就要升级。
3. 重量级锁 (Heavyweight Lock)

场景:多个线程同时竞争锁,自旋失败,或者竞争激烈。

原理

  • 当轻量级锁升级为重量级锁时,锁标志的状态值变为 10,Mark Word 中存储的是指向**重量级锁(ObjectMonitor)**的指针。
  • 此时等待锁的线程会被阻塞 (Block),进入内核态。
  • ObjectMonitor:这是基于操作系统的 Mutex Lock(互斥锁)实现的。
    • _owner:指向持有 ObjectMonitor 对象的线程。
    • _EntryList:处于等待锁 block 状态的线程,会被加入到该列表。
    • _WaitSet:处于 wait 状态的线程,会被加入到该列表。

代价

  • 线程的阻塞和唤醒需要操作系统介入,需要在用户态内核态之间切换,由于 CPU 上下文切换的成本非常高,所以称为“重量级”。

三、 锁升级流程图 (可视化)

为了让你更直观地理解,我画了一个 Mermaid 流程图来描述这个升级过程:

graph TD
    Start((对象创建)) --> NoLock[无锁状态<br/>MarkWord: hash|age|0|01]
    
    NoLock -- 线程A访问 --> BiasCheck{是否开启偏向锁?}
    
    BiasCheck -- Yes --> BiasedLock[偏向锁<br/>MarkWord: ThreadA_ID|age|1|01]
    BiasCheck -- No --> LightLock
    
    BiasedLock -- 线程A再次访问 --> BiasedLock
    
    BiasedLock -- 线程B尝试获取锁 --> RevokeBias[撤销偏向锁<br/>(需等待全局安全点)]
    
    RevokeBias --> IsALive{线程A是否存活<br/>且持有锁?}
    
    IsALive -- No (A已结束) --> NoLock
    IsALive -- Yes (A还在用) --> UpgradeLight[升级为轻量级锁]
    
    UpgradeLight --> LightLock[轻量级锁<br/>MarkWord: 指向栈中Lock Record|00]
    
    LightLock -- 线程A/B交替执行 --> LightLock
    
    LightLock -- 发生竞争 (CAS失败) --> Spin{自旋<br/>(Adaptive Spin)}
    
    Spin -- 自旋成功 --> LightLock
    Spin -- 自旋失败/次数过多 --> UpgradeHeavy[升级为重量级锁]
    
    UpgradeHeavy --> HeavyLock[重量级锁<br/>MarkWord: 指向Monitor|10]
    
    HeavyLock -- 线程阻塞/挂起 --> WaitOS[等待操作系统唤醒<br/>(内核态切换)]

四、 图解内存结构变化

为了更形象,我们画一下内存中 Mark Word 的变化:

1. 初始/偏向阶段
线程 A 的栈帧
+-------------+
| Lock Record |  <-- 不需要存 Displaced Mark Word
+-------------+

堆中的对象 (Object)
+---------------------------------------------------+
| Mark Word: [ Thread A ID | Epoch | Age | 1 | 01 ] |  <-- 记录了线程A的ID
+---------------------------------------------------+
2. 轻量级锁阶段 (线程A持有,线程B来抢,升级)
线程 A 的栈帧                       线程 B 的栈帧
+-----------------------+          +-----------------------+
| Lock Record (LR)      |          | Lock Record (LR)      |
| [ Original Mark Word ]|          | [ Original Mark Word ]|
+-----------------------+          +-----------------------+
           ^                                |
           | (CAS 成功指向 A)                | (CAS 失败)
           |                                v
堆中的对象 (Object)
+---------------------------------------------------+
| Mark Word: [ ptr to A's Lock Record      | 00 ]   |
+---------------------------------------------------+
3. 重量级锁阶段 (自旋失败,膨胀)
堆中的对象 (Object)
+---------------------------------------------------+
| Mark Word: [ ptr to ObjectMonitor        | 10 ]   |
+---------------------------------------------------+
           |
           v
C++ ObjectMonitor 对象 (OS Mutex)
+------------------------------------------+
| _owner:     Thread A                     |
| _EntryList: [ Thread B, Thread C... ]    |  <-- 都在阻塞等待
| _WaitSet:   [ ... ]                      |
+------------------------------------------+

五、 总结对比

锁类型

触发场景

优点

缺点

适用场景

偏向锁

只有一个线程访问

加锁/解锁不需要CAS,只需对比ThreadID,消耗极低

若存在竞争,撤销锁需要等待安全点,开销大

只有一个线程访问同步块的场景

轻量级锁

多个线程交替执行,无同时竞争

竞争的线程不会阻塞,使用CAS和自旋,响应速度快

若始终得不到锁,长时间自旋会消耗CPU

追求响应时间,同步块执行速度非常快

重量级锁

多线程同时竞争,竞争激烈

线程被阻塞,不消耗CPU

线程阻塞/唤醒需内核态切换,响应慢,吞吐量低

追求吞吐量,同步块执行时间较长

六、 一句话总结

锁升级就是JVM为了避免每次都去请求操作系统层面的互斥锁(重),而先尝试在JVM层面利用对象头标记(偏)和CAS自旋(轻)来解决冲突的优化手段。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐