synchronized锁升级全流程解析

0 阅读5分钟

synchronized 锁升级全流程解析

在 Java 开发者的眼中,synchronized 曾经是“笨重”的代名词。早期版本的 synchronized 一上来就直接调用操作系统的底层同步机制,导致线程上下文切换频繁,效率极低。

但在 Java 6 之后,HotSpot 虚拟机为了优化性能,对 synchronized 引入了极其精妙的“锁升级”机制。今天,我们就通过深度分析对象头、CAS 竞争和 Monitor 机制,彻底讲透锁升级的原理。


1. 这篇文章要解决什么问题?

早期的 synchronized 只有 重量级锁 一种形式。每当一个线程请求锁,都会触发:

  • 用户态与内核态的切换
  • 线程的挂起与唤醒(操作系统层面)。

这种操作极其耗时。但在实际业务中,科学家们发现:

  1. 很多时候,锁其实只会被 同一个线程多次访问
  2. 即使有竞争,往往也是 交替执行,竞争通过简单的自旋就能解决。

锁升级的目的,就是为了根据实际的竞争激烈程度,逐步“按需加重”锁的开销。


2. 核心原理:对象头里的“秘密花园”

要理解锁升级,必须先看懂 Java 对象头(Object Header)。每个 Java 对象在内存中都带有一个“头”,其中的 Mark Word 是控制锁状态的核心。

Mark Word 的动态平衡

在 64 位 JVM 中,Mark Word 占 8 个字节(64 bit)。它是一个“变色龙”,在不同的锁状态下,位域的含义完全不同:

锁状态25 bit31 bit1 bit4 bit1 bit (偏向位)2 bit (锁标志位)
无锁未使用hashCode0分代年龄001
偏向锁ThreadID (54bit)Epoch (2bit)0分代年龄101
轻量级锁指向栈中锁记录 (Lock Record) 的指针00
重量级锁指向互斥量 (Monitor) 的指针10

Mark Word 在不同锁状态下的结构变化对比图.png


3. 流程/机制描述:一步步进阶的锁

第一阶段:偏向锁 (Biased Locking) —— 一个人的舞台

核心逻辑:如果锁总是被同一个线程获取,那么标记一下线程 ID 就行了,连 CAS 都不需要。

  • 动作:当线程第一次访问同步块,Mark Word 会记录下当前线程 ID,偏向位置为 1。
  • 进入:下次该线程再来,只需检查 ThreadID 是否匹配,匹配直接通过。
  • 撤销:当有另一个线程尝试竞争时,偏向锁会被撤销,根据对象是否仍锁住决定升级。

第二阶段:轻量级锁 (Lightweight Locking) —— 彬彬有礼的竞争

核心逻辑:当出现了多个线程交替访问,但没有激烈竞争时,使用 CAS 替换对象头。

  • 动作:线程在自己的栈帧中开辟空间(Lock Record),并将对象的 Mark Word 拷贝过去。
  • CAS 争夺:线程尝试用 CAS 将对象头的 Mark Word 替换为指向自己栈中 Lock Record 的指针。
  • 自旋:如果 CAS 失败,说明有轻微竞争,线程不会挂起,而是“原地踏步”(自旋)一会儿继续尝试。

第三阶段:重量级锁 (Heavyweight Locking) —— 实打实的冲突

核心逻辑:当自旋次数过多,或者多个线程同时激烈争夺时,锁变得极其沉重。

  • 升级触发:轻量级锁 CAS 失败次数达到限度,或者同时有多个线程在自旋等待。
  • Monitor 介入:锁膨胀为重量级锁,Mark Word 指向 ObjectMonitor 对象。
  • 挂起:除了持有锁的线程,其它线程全部进入 WaitSetEntryList 等待区,被操作系统挂起,进入阻塞状态。

synchronized 锁升级全流程图(偏向 - 轻量 - 重量).png


4. 关键代码/示例

字节码维度的 synchronized

通过 javap -c 观察代码,你会发现同步块是由配对的指令控制的。

public class SyncExample {
    public void syncBlock() {
        synchronized (this) {
            // 业务逻辑
        }
    }
}

对应的字节码摘要:

 0: aload_0
 1: dup
 2: astore_1
 3: monitorenter // 代表锁的开始
 ...
 15: monitorexit // 代表正常退出
 ...
 21: monitorexit // 代表异常退出(确保锁一定释放)

如何观察对象头?

在实际工作中,我们可以使用 JOL (Java Object Layout) 工具来实时观察 Mark Word 的位变化。

import org.openjdk.jol.info.ClassLayout;

/**
 * 使用 JOL 观察对象头锁标志位
 */
public class JOLDemo {
    public static void main(String[] args) {
        Object obj = new Object();
        // 1. 无锁状态
        System.out.println("--- 无锁状态 ---");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());

        synchronized (obj) {
            // 2. 这种情况下可能是偏向锁或轻量锁(取决于 JVM 启动参数)
            System.out.println("--- 锁住状态 ---");
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        }
    }
}

注:由于 JVM 默认偏向锁有 4s 延迟,直接运行可能先看到轻量级锁。


5. 常见误区

误区 1:锁升级是可逆的

反驳:在 HotSpot 虚拟机中,锁升级通常是 单向不可逆 的(偏向锁 -> 轻量级锁 -> 重量级锁)。虽然在偏向锁撤销后可能重新进入偏向状态,但轻量锁一旦升级为重量锁,通常不会再自动“降级”回去。

误区 2:自旋锁就是轻量级锁

反驳:自旋(Spin)只是一种 手段,它发生在轻量级锁升级为重量级锁的过程中。轻量级锁本身是利用 CAS 替换指针,而当 CAS 失败时,才会开启自旋优化以期不挂起线程。


6. 实际工作中怎么用?

  1. 预估并发压力: 如果你的场景天生就是极高并发、大量争抢(如秒杀核心逻辑),synchronized 可能会迅速膨胀为重量级锁。此时可以考虑 ReentrantLock 提供的更丰富的 API。

  2. JVM 调优建议

    • 偏向锁延迟:默认 4 秒延迟开启。如果你的应用一启动就有大量线程竞争,可以考虑通过 -XX:BiasedLockingStartupDelay=0 来关闭延迟,或者彻底禁用偏向锁。
    • 现代趋势:值得注意的是,JDK 15 之后已经默认禁用了偏向锁,因为维护偏向锁撤销的成本在现代多核架构下有时反而得不偿失。

总结

synchronized 的设计哲学体现了 Java 性能优化的核心思想:平路加速,上坡减挡。在没有竞争时追求极致性能,在竞争激烈时保证结果正确。理解锁升级,是你通向 Java 并发架构师的必经之路。