Skip to content

JDK 6 之后锁升级机制:偏向锁 → 轻量级锁 → 重量级锁

你知道吗?JDK 6 之前,synchronized 被称为「性能杀手」。

那时候,一个简单的 synchronized 代码块,就可能导致线程阻塞——而线程阻塞需要操作系统介入,用户态和内核态的切换,少说也要几微秒。

但 JDK 6 之后,JVM 团队花了三年时间重新设计 synchronized,引入了一套精妙的锁升级机制

从此,synchronized 的性能可以和 Lock 抗衡了。


锁升级路线图

┌─────────┐     线程第一次访问      ┌─────────┐     出现竞争      ┌─────────┐     竞争激烈      ┌─────────┐
│  无锁   │ ───────────────────▶ │  偏向锁  │ ─────────────▶ │ 轻量级锁 │ ──────────────▶ │ 重量级锁 │
└─────────┘                      └─────────┘                 └─────────┘                  └─────────┘
     │                                 │                           │                           │
     │                                 │                           │                           │
  正常状态                      单线程访问同步块                 线程交替执行                 多线程同时竞争
                                 (无竞争)                    (无实际竞争)               (需要OS介入)

重要特性:这把锁只能升级,不能降级——这就是所谓的「锁膨胀」。


阶段一:无锁 → 偏向锁

什么时候进入偏向锁?

当一个线程第一次访问同步块时,JVM 会尝试用偏向锁。

偏向锁的获取过程

线程A 首次进入同步块


┌─────────────────────────────────────────┐
│  检查 Mark Word:biased_lock = 0?      │
└─────────────────────────────────────────┘
        │ 是

┌─────────────────────────────────────────┐
│  CAS(0, thread_id)                      │
│  将 Mark Word 的 biased_lock 设为 1      │
│  并写入当前线程 ID                        │
└─────────────────────────────────────────┘
        │ 成功

    偏向锁获取成功
    线程A 进入同步块,无需任何同步操作

为什么偏向锁这么快?

因为根本不需要 CAS!再次进入同步块时,只需检查 Mark Word 中的线程 ID 是否是自己的。

java
// 偏向锁状态下的同步块执行
if (markWord.threadId == currentThreadId) {
    // 跳转到同步代码块
}

阶段二:偏向锁 → 轻量级锁

什么情况会撤销偏向锁?

  1. 另一个线程尝试竞争:线程 B 来访问同一个同步块
  2. 调用 hashCode():计算过 hashCode 的对象无法保持偏向锁
  3. 偏向锁等待超时BiasedLockingStartupDelay 默认 4 秒

撤销过程

线程B 尝试进入同步块


┌─────────────────────────────────────────┐
│  发现对象处于偏向锁状态                   │
│  且偏向的线程A 正在执行                   │
└─────────────────────────────────────────┘


┌─────────────────────────────────────────┐
│  偏向锁撤销(升级为轻量级锁)              │
└─────────────────────────────────────────┘


    线程B 进入轻量级锁获取流程

阶段三:轻量级锁 → 重量级锁

什么情况会膨胀为重量级锁?

  1. CAS 竞争失败:线程 B 自旋一定次数后仍未获取锁
  2. 自旋超过阈值:JVM 自适应决定

膨胀过程

线程B CAS 获取轻量级锁失败


┌─────────────────────────────────────────┐
│  在线程B 栈帧中创建锁记录(Lock Record)   │
│  并将 Mark Word 复制到锁记录              │
└─────────────────────────────────────────┘


┌─────────────────────────────────────────┐
│  CAS(对象头, 锁记录指针, 线程A的MarkWord) │
└─────────────────────────────────────────┘

        ├────────── 成功 ───────────▶ 线程B 获得轻量级锁

        └───── 失败(线程A还没释放)──▶ 膨胀为重量级锁
                                        线程B 进入阻塞队列

三种锁状态对比

特性偏向锁轻量级锁重量级锁
适用场景单线程访问多线程交替访问多线程竞争
性能开销最低(仅首次CAS)较低(CAS自旋)较高(OS阻塞)
实现原理Mark Word 存线程IDMark Word 指向栈锁记录Mark Word 指向Monitor
CPU 消耗自旋消耗CPU线程阻塞不消耗CPU
响应速度立即稍慢(自旋)慢(线程切换)

为什么锁升级不可逆?

这是面试常问的问题。核心原因:Mark Word 的空间不够用

无锁状态:25bit hashCode + age + lock
偏向锁:23bit threadID + epoch + age + lock
轻量级锁:30bit 指向锁记录的指针 + lock
重量级锁:30bit 指向Monitor的指针 + lock

偏向锁需要存线程 ID,轻量级锁需要存指针——这两个信息不能同时存在

一旦升级到轻量级锁,原来的偏向信息就丢失了,无法退回偏向锁。


实际性能对比

用一个小测试来验证:

java
public class LockUpgradeTest {
    
    private static final int ITERATIONS = 10_000_000;
    
    // 无竞争场景
    public static void singleThread() throws Exception {
        Object lock = new Object();
        long start = System.nanoTime();
        for (int i = 0; i < ITERATIONS; i++) {
            synchronized (lock) {
                // 空操作
            }
        }
        System.out.println("单线程耗时: " + (System.nanoTime() - start) / 1_000_000 + "ms");
    }
    
    public static void main(String[] args) throws Exception {
        singleThread();
    }
}

理论预期结果

  • 偏向锁:~50ms(几乎无开销)
  • 轻量级锁:~100ms(有自旋开销)
  • 重量级锁:~500ms+(线程阻塞开销)

JDK 对锁升级的影响

JDK 15+ 的变化

JDK 1.6 - 1.14:  偏向锁默认开启
JDK 15:          偏向锁默认关闭(--XX:+UseBiasedLocking)
JDK 18:          偏向锁彻底移除

原因:偏向锁在多线程竞争激烈的场景下,反而会带来额外的撤销开销。

关闭偏向锁

bash
# 关闭偏向锁(高并发场景推荐)
-XX:-UseBiasedLocking

# 设置偏向锁延迟(等待JVM启动完成后再开启)
-XX:BiasedLockingStartupDelay=0

实战建议

1. 高并发场景

bash
# 关闭偏向锁,减少撤销开销
-XX:-UseBiasedLocking

2. 低并发场景

保持默认配置,偏向锁能带来最好的性能。

3. 锁竞争激烈但需要低延迟

考虑使用 ReentrantLock,可以避免 synchronized 的不公平性:

java
ReentrantLock lock = new ReentrantLock(true);  // 公平锁

面试追问方向

  1. synchronized 和 ReentrantLock 在锁升级方面有什么区别? synchronized 会自动升级,ReentrantLock 不会(它直接使用 AQS,没有偏向锁和轻量级锁的概念)。

  2. 为什么 JDK 15 要废弃偏向锁? 偏向锁在竞争激烈时需要频繁撤销,开销反而大于无锁状态。

  3. 锁升级过程中,Mark Word 变化了几次? 最多三次:无锁→偏向锁→轻量级锁→重量级锁。

  4. 如果一个对象的偏向锁被撤销后,还能再次偏向吗? 可以,但需要等到批量重偏向阈值。


留给你的思考题

假设这样一个场景:

时间线:
T1: 线程A 进入同步块,获取偏向锁
T2: 线程A 退出同步块,但对象仍被线程A 使用
T3: 线程B 来竞争这个对象的锁
T4: 偏向锁被撤销,轻量级锁登场
T5: 线程B 自旋失败,膨胀为重量级锁

问题

  1. T2 时刻,偏向锁会释放吗?为什么?
  2. 如果 T3 发生在 T2 之前(即线程A 还在同步块中),偏向锁的撤销过程有什么不同?
  3. 如果线程A 和线程B 交替进入同一个同步块,锁会在哪些状态之间切换?

基于 VitePress 构建