synchronized 的锁是怎么升级的?

🔒 一、HotSpot 团队为什么要设计锁升级机制

在 JDK 1.0 时代,synchronized 直接对应操作系统的 Mutex(互斥量)。每次加锁都要陷入内核态,哪怕只有一条线程在访问、根本不存在竞争。这就像你住的小区只有一个停车位,每次出门都要跑去物业办公室办手续——哪怕车位从来没人跟你抢。

2004 年,随着 JDK 5 和 JSR 133 的发布,Java 并发性能成了焦点。道格·李的 JUC 提供了 ReentrantLockSemaphore 等无锁/CAS 工具,它们的性能远超 synchronized。一时间,社区舆论变成了"别用 synchronized,它是重量级锁、太慢"。

synchronized 有一个 JUC 工具永远比不了的优势:它是语言内置的——不需要显式 lock() / unlock(),不用怕忘了释放锁导致死锁。 如果因为性能差就被开发者抛弃,将是 Java 语言的重大损失。

HotSpot JVM 团队(主要贡献者包括 David Dice 等人)在 JDK 6 中给出了答案:锁升级(Lock Escalation)机制。核心思路是——根据"大多数锁没有竞争"这个经验事实,让 synchronized 从最轻的模式开始:

  • 偏向锁(Biased Locking):只有一条线程用这个锁时,Mark Word 里记个线程 ID 就行,不需要 CAS,几乎零开销。
  • 轻量级锁(Lightweight Locking):两个线程交替使用(无实际竞争)时,在栈上分配 Lock Record,用 CAS 交换 Mark Word。
  • 重量级锁(Heavyweight Locking):真有竞争时,才膨胀为 OS Mutex,线程阻塞等待。

这个设计让 synchronized 在大多数实际场景中的性能追平甚至超过了 ReentrantLock。锁升级的判断依据只有 Mark Word 中的 3 个比特位。

📦 2️⃣ 二、对象头(Object Header):JVM 如何表示"锁"

📦 2.1 堆中对象的内存布局

Java 对象在堆中的内存布局分为四个区域。理解这个布局是理解锁升级的前提:

flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef data fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
    subgraph OBJ[堆中的Java对象]
        MW[Mark Word\n8字节] --> KP[Klass Pointer\n4字节 压缩后]
        KP --> DATA[实例数据\n变长]
        DATA --> PAD[对齐填充\n补齐到8字节倍数]
    end
    MW -->|标记字| MW_DETAIL[锁状态 / hash / GC年龄\n共64bit]
    KP -->|指向方法区| KLASS[InstanceKlass\n类元数据]

class DATA,KLASS,OBJ data;
class KP,MW,MW_DETAIL,PAD process;
区域大小(64位JVM,开启压缩)说明
Mark Word8 字节(64 位)存储锁状态、hash、GC 年龄。不同锁状态下同一块内存复用为不同结构
Klass Pointer4 字节(默认开启压缩指针 -XX:+UseCompressedOops指向方法区中该对象所属的类元数据(InstanceKlass)
实例数据变长对象中声明的实例字段(包括从父类继承的)
对齐填充补齐用HotSpot 要求对象起始地址是 8 字节的整数倍,不足则填充

🔢 2.2 Mark Word 的五种状态

Mark Word 的 64 位中,最后 3 位是 biased_lock (1 位)+ lock (2 位)。JVM 通过读取这 3 位判断对象处于哪种锁状态。状态不同,前面 61 位的含义也完全不同:

flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
    subgraph NOLOCK[无锁状态]
        direction LR
        A1["unused\n25bit"]
        A2["identity_hashcode\n31bit"]
        A3["unused\n1bit"]
        A4["GC age\n4bit"]
        A5["biased_lock\n0"]
        A6["lock\n01"]
    end
    subgraph BIASED[偏向锁]
        direction LR
        B1["Thread ID\n54bit"]
        B2["epoch\n2bit"]
        B3["unused\n1bit"]
        B4["GC age\n4bit"]
        B5["biased_lock\n1"]
        B6["lock\n01"]
    end
    subgraph LIGHT[轻量级锁]
        direction LR
        C1["ptr_to_lock_record\n62bit"]
        C2["lock\n00"]
    end
    subgraph HEAVY[重量级锁]
        direction LR
        D1["ptr_to_ObjectMonitor\n62bit"]
        D2["lock\n10"]
    end
    subgraph GC[GC标记]
        direction LR
        E1["forward_ptr\n62bit"]
        E2["lock\n11"]
    end

class A1,A2,A3,A4,A5,A6,B1,B2,B3,B4,B5,B6,BIASED,C1,C2,D1,D2,E1,E2,GC,HEAVY,LIGHT,NOLOCK process;

将这五种状态汇总为一张判断表。JVM 在进入 synchronized 块时,读取的就是这最后 3 位:

biased_lock(第 3 位)lock(低 2 位)3 位整体值锁状态Mark Word 中存储的内容
001001无锁31 位 identity hashcode + 4 位 GC 年龄
101101偏向锁54 位偏向线程 ID + 2 位 epoch + 4 位 GC 年龄
00×00轻量级锁62 位指向栈中 Lock Record 的指针
10×10重量级锁62 位指向 ObjectMonitor 的指针
11×11GC 标记62 位转发指针(forwarding pointer)

其中 表示在轻量级锁、重量级锁和 GC 标记状态下,biased_lock 位被指针复用,没有独立含义。

HotSpot 中这段逻辑定义在 markOop.hpp 中:

// hotspot/src/share/vm/oops/markOop.hpp(关键枚举值)
enum {
    locked_value             = 0,    // 00 → 轻量级锁
    unlocked_value           = 1,    // 01 → 无锁
    monitor_value            = 2,    // 10 → 重量级锁
    marked_value             = 3,    // 11 → GC 标记
    biased_lock_pattern      = 5     // 101(二进制)→ 偏向锁
};

// JVM 判断锁状态的核心逻辑
bool is_biased()  const { return mask_bits(value, biased_lock_mask_in_place); }
bool is_neutral() const { return mask_bits(value, biased_lock_mask_in_place | lock_mask_in_place) == unlocked_value; }

JVM 在每次进入 synchronized 块时,只需要用 mark_word & 0b111 取出低 3 位,然后走对应的分支。这个判断只需要一条位与指令,非常快。

📌 2.3 用 JOL 观察 Mark Word

JOL(Java Object Layout)是 OpenJDK 提供的工具,可以直接打印对象的内存布局。下面用 JOL 观察一把锁从无到有过程中 Mark Word 的变化:

// 依赖: org.openjdk.jol:jol-core:0.16
import org.openjdk.jol.info.ClassLayout;

public class MarkWordViewer {
    public static void main(String[] args) {
        Object lock = new Object();

        // 打印无锁状态的对象头
        System.out.println("=== 无锁状态 ===");
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());

        synchronized (lock) {
            // 打印加锁后的对象头
            System.out.println("=== 加锁后(当前线程持有锁) ===");
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }

        // 打印解锁后的对象头
        System.out.println("=== 解锁后 ===");
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    }
}

输出分析(64 位 JVM):

=== 无锁状态 ===
OFFSET  SIZE   TYPE DESCRIPTION            VALUE
     0     4        (object header)        01 00 00 00
     4     4        (object header)        00 00 00 00
     8     4        (object header)        00 10 00 00   ← Klass Pointer
    12     4        (object alignment gap)

=== 加锁后(当前线程持有锁) ===
OFFSET  SIZE   TYPE DESCRIPTION            VALUE
     0     4        (object header)        98 f3 1a 03   ← Mark Word 变了!
     4     4        (object header)        00 00 00 00
     8     4        (object header)        00 10 00 00
    12     4        (object alignment gap)

无锁时 Mark Word 的 8 个字节是 00 00 00 00 00 00 00 01(小端序),末尾的 01lock=01, biased_lock=0,无锁状态。

加锁后 Mark Word 变了一个地址值(如 03 1a f3 98),这是因为 JVM 把 Mark Word 改成了指向栈中 Lock Record 的指针——lock=00,轻量级锁。

⬆️ 三、锁升级的总体路径

在展开每一级锁之前,先建立全局视角。synchronized 的锁升级是一个 只升不降 的单向过程:

stateDiagram-v2
    [*] --> 无锁
    无锁 --> 偏向锁 : 第一个线程获取锁\nCAS将ThreadID写入Mark Word
    偏向锁 --> 轻量级锁 : 另一个线程尝试获取锁\n撤销偏向
    轻量级锁 --> 重量级锁 : CAS自旋超过阈值\n或等待队列长度>1
    重量级锁 --> [*]

    note right of 无锁 : Mark Word低3位=001
    note right of 偏向锁 : Mark Word低3位=101
    note right of 轻量级锁 : Mark Word低2位=00
    note right of 重量级锁 : Mark Word低2位=10

为什么只升不降?因为降锁需要判断"是否所有线程都已经离开同步块"——这个判断本身需要全局同步,成本比直接升为重量级锁更高。HotSpot 的选择是:“宁可留在高级别,也不为降级付出额外的判断成本”。唯一的例外是偏向锁的批量撤销(后面详述)。

四级锁的核心区别:

维度偏向锁轻量级锁重量级锁
存储位置Mark Word 存线程 IDMark Word 存指向栈中 Lock Record 的指针Mark Word 存指向 Native 内存中 ObjectMonitor 的指针
加锁操作比较线程 ID(无 CAS)CAS 设置 Mark WordCAS 设置 ObjectMonitor._owner,失败则 OS Mutex 挂起
解锁操作无操作(退出同步块时不改 Mark Word)CAS 恢复 Mark Word设置 _owner=null,唤醒 EntryList 队头线程
竞争处理撤销并升级自旋重试 CAS → 膨胀OS Mutex 管理等待队列
适用场景同一线程反复进入两个线程交替执行多个线程同时争抢

接下来逐级展开每一把锁的内部机制。

4️⃣ 四、偏向锁(Biased Locking)

❓ 4.1 解决的问题

很多类(如 StringBufferVector)的设计中,同步方法被频繁调用,但大多数情况下只有 同一个线程 在调用。在没有偏向锁时,即使没有第二个线程,每次进入 synchronized 块也要执行一次 CAS 操作(至少几十个 CPU 周期)。

偏向锁的核心思路:<span style="color:red">如果一把锁从始至终只被一个线程获取,就不需要每次加锁都执行 CAS——在 Mark Word 中记下这个线程的 ID,以后这个线程再来的时候,只看一眼 Mark Word 就够了。

🔒 4.2 加锁过程(逐步拆解)

线程 T1 第一次获取偏向锁

步骤1:检查 Mark Word 低3位
       mark & 0b111
       ├── == 001(无锁,未偏向) → 可以偏向
       │   继续步骤2
       └── == 101(已偏向)
           比较 Mark Word 中的ThreadID
           ├── ThreadID == T1 → 直接进入(无 CAS!一步完成)
           └── ThreadID != T1 → 撤销偏向(见4.4节)

步骤2:CAS 写入偏向信息
       构造新的Mark Word:
       [ThreadID(T1) | epoch | unused | age | biased_lock=1 | lock=01]
       CAS(&obj.mark_word, old_value, new_value)
       ├── 成功 → 偏向到T1,进入同步块
       └── 失败 → 有其他线程同时CAS,升级为轻量级锁

关键点:<span style="color:red">偏向锁的第一次获取需要 CAS,但之后同一个线程再次获取时不需要任何原子操作——只需要一次普通的 64 位读取和比较。

为什么退出同步块时不释放偏向锁 ?这是偏向锁和另外两级锁的本质区别:偏向锁的持有者退出同步块时, 不修改 Mark Word 。Mark Word 仍然保留着偏向线程的 ID。只有当另一个线程尝试获取这把锁时,才触发撤销。

// 偏向锁"不释放"的演示
Object lock = new Object();

// T1: 第一次synchronized → CAS写入偏向T1
synchronized (lock) { /* ... */ }
// T1退出同步块,但Mark Word仍然是偏向T1!

// T1: 第二次synchronized → 直接比较ThreadID,无需CAS
synchronized (lock) { /* ... */ }
// 仍然是偏向T1,锁根本没动过

🎯 ↩️ 4.3 偏向锁撤销(Revocation)

撤销发生在 VM 安全点(Safepoint) 。在安全点,所有 Java 线程被暂停,JVM 可以安全地检查任意线程的栈帧。

撤销的完整流程:

sequenceDiagram
    participant T1 as 线程T1\n偏向持有者
    participant MW as Mark Word
    participant T2 as 线程T2\n竞争线程
    participant VM as JVM\nSafepoint

    T2->>MW: 尝试获取锁\n读取低3位 = 101(偏向锁)\nThreadID = T1(不是自己!)
    T2->>VM: 发起偏向锁撤销请求
    VM->>VM: 等待全局Safepoint\n所有线程暂停
    VM->>T1: 检查T1的栈帧
    alt T1已退出同步块
        VM->>MW: 恢复为无锁(001)\n然后T2通过CAS获取轻量级锁
    else T1仍在同步块内
        VM->>VM: 膨胀为轻量级锁\nT1持有该轻量级锁\nT2竞争轻量级锁
    end
    VM->>VM: 恢复所有线程
    Note over T2: T2继续执行

撤销的代价很高——Safepoint 意味着所有线程暂停。这就是为什么在激烈竞争场景下偏向锁会拖累性能:每次撤销都要等全局暂停。

↩️ 4.4 批量重偏向与批量撤销

如果一个类的对象频繁发生偏向锁撤销(撤销次数达到阈值),JVM 会认为偏向策略对这类对象失效。HotSpot 采取两步策略:

阶段触发条件JVM 行为
批量重偏向某个类的偏向锁撤销次数在 20 秒内达到20 次-XX:BiasedLockingBulkRebiasThreshold=20JVM 认为这些对象只是偏向了错误的线程(不是偏向策略本身有问题)。将该类所有对象的 epoch 值 + 1。epoch 值变了之后,旧的偏向线程 ID 失效,对象可以被重新偏向到新线程,但不需要走撤销流程
批量撤销批量重偏向后,撤销次数继续增加,总撤销次数达到40 次-XX:BiasedLockingBulkRevokeThreshold=40JVM 认为这个类根本不适合 偏向锁。将该类的所有对象标记为"不可偏向",此后新对象在创建时就以无锁状态初始化(biased_lock=0),不再尝试偏向

epoch 的作用:epoch 是一个 2 位的版本号,存储在类的元数据中,也复制到每个对象的 Mark Word 中。当 JVM 批量重偏向时,增加类级别的 epoch,但旧对象 Mark Word 中的 epoch 是旧的——下次线程访问这些旧对象时,发现 epoch 不匹配,就会 CAS 写入新线程 ID(这不算撤销,只是一次 CAS)。

注意:JDK 15 起偏向锁默认禁用(-XX:+UseBiasedLocking 默认 false),JDK 18 中 UseBiasedLocking 被标记为废弃(deprecated)。原因是现代应用大多使用线程池,线程竞争频繁,偏向锁的撤销成本超过了它的收益。

🧪 4.5 偏向锁的 JOL 验证

// JVM参数: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
// BiasedLockingStartupDelay=0 禁用偏向锁的4秒启动延迟
public class BiasedLockDemo {
    public static void main(String[] args) throws Exception {
        Object lock = new Object();

        System.out.println("=== 刚创建(可偏向,但还未偏向) ===");
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());

        synchronized (lock) {
            System.out.println("=== 第一次加锁(偏向到main线程) ===");
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }

        System.out.println("=== 解锁后(仍偏向main线程) ===");
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());

        // 另一个线程竞争 → 触发撤销
        new Thread(() -> {
            synchronized (lock) {
                System.out.println("=== T2加锁(撤销后升级为轻量级锁) ===");
                System.out.println(ClassLayout.parseInstance(lock).toPrintable());
            }
        }).start();
        Thread.sleep(1000);
    }
}

输出解读

  • 刚创建时:01 00 00 00 00 00 00 00 → 低 3 位 001,无锁但 可偏向 (biased_lock=0 只是因为还没偏过)
  • 第一次加锁:01 00 00 00 00 20 d0 1f → 这 8 字节的高位包含了线程 ID,低 3 位 101 = 已偏向
  • 解锁后:Mark Word 不变!仍然是偏向 main 线程的
  • T2 竞争后:Mark Word 变了一个 62 位指针,低 2 位 00 = 轻量级锁

5️⃣ 五、轻量级锁(Lightweight Lock)

🏗️ 5.1 数据结构:Lock Record

轻量级锁的核心数据结构是 Lock Record (BasicLock),分配在 线程的栈帧 中。每个进入 synchronized 块的线程,在自己的栈上分配一个 Lock Record。

flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef data fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
    subgraph STACK[线程T1的栈]
        SF[栈帧]
        LR[Lock Record]
        LR_DW["displaced_mark_word\n(保存对象的原始Mark Word)"]
        LR_OWNER["_owner\n(指向被锁的Java对象)"]
        SF --- LR
        LR --- LR_DW
        LR --- LR_OWNER
    end
    subgraph HEAP[堆]
        OBJ[Java对象lock]
        MW["Mark Word\n(指向Stack中的Lock Record)\nlock=00"]
    end
    LR_DW -.->|"保存了"| OLD_MW["原始Mark Word\n(无锁时的值)"]
    MW -->|"指针→"| LR
    LR_OWNER -.->|"指向"| OBJ

class HEAP data;
class LR,LR_DW,LR_OWNER,MW,OBJ,OLD_MW,SF,STACK process;

HotSpot 中 Lock Record 的定义(basicLock.hpp):

class BasicLock {
    volatile markOop _displaced_header;  // 保存对象原始的 Mark Word
    // BasicObjectLock 中包含一个 _lock(BasicLock)和一个 _obj(指向被锁对象的指针)
};

class BasicObjectLock {
    BasicLock _lock;
    oop       _obj;    // 指向被 synchronized 锁住的那个 Java 对象
};

Lock Record 的两个核心字段:

  • _displaced_header:保存锁对象 原来的 Mark Word。解锁时需要把它恢复回去
  • _obj(在 BasicObjectLock 中):指向被锁的 Java 对象,用于关联栈上的锁记录和堆中的对象

🔒 5.2 加锁过程(逐行拆解)

轻量级锁的加锁,本质是一次 CAS 操作:<span style="color:red">尝试把锁对象的 Mark Word 从"无锁值"替换为"指向当前线程 Lock Record 的指针"。

sequenceDiagram
    participant T1 as 线程T1
    participant STACK as T1栈帧
    participant MW as 对象Mark Word

    T1->>STACK: 1. 在线程栈上\n创建Lock Record
    T1->>MW: 2. 读取当前Mark Word\n(假设无锁:hash|age|001)
    T1->>STACK: 3. 将Mark Word原值保存到\nLock Record.displaced_mark_word
    T1->>MW: 4. CAS(&mark_word,\n  old = 无锁值,\n  new = ptr_to_lock_record | 00)
    alt CAS成功
        MW->>MW: Mark Word变为\nptr_to_LockRecord | lock=00
        Note over T1: 获得轻量级锁\n进入同步块
    else CAS失败
        Note over T1: 见5.3节:自旋或膨胀
    end

对应的 HotSpot 汇编入口是 InterpreterRuntime::monitorenter(解释执行)或 C2 编译器内联生成的锁代码。核心逻辑在 synchronizer.cppObjectSynchronizer::fast_enter() 中。

用伪代码表达加锁逻辑:

// 轻量级锁加锁(概念层面,非 HotSpot 逐行实现)
void lightweightLock(Object obj) {
    // 步骤1:在当前线程栈帧中分配 Lock Record
    BasicLock lockRecord = new BasicLock();

    // 步骤2-3:复制原始 Mark Word 到 Lock Record
    markWord currentMark = obj.readMarkWord();
    lockRecord.setDisplacedHeader(currentMark);

    // 步骤4:CAS——将 obj.markWord 替换为指向 lockRecord 的指针
    markWord newMark = encodePtr(lockRecord) | 0b00;  // 低2位=00(轻量级锁)
    if (CAS(&obj.markWord, currentMark, newMark)) {
        // CAS 成功 → 获得了轻量级锁
        return;
    }

    // CAS 失败 → 需要判断是否是自己已经持有了(锁重入)
    if (isLockRecordOfCurrentThread(obj.markWord)) {
        // 锁重入:再创建一个 Lock Record,displaced_header = null
        lockRecord.setDisplacedHeader(null);
        return;
    }

    // 其他线程持有 → 膨胀为重量级锁
    inflateToHeavyweight(obj);
}

📈 5.3 CAS 失败之后:自旋与膨胀

如果 CAS 失败,线程不会立即挂起。原因:<span style="color:red">另一个线程可能只是短暂持有轻量级锁(比如只执行了几条指令就退出同步块),如果当前线程立即阻塞,用户态到内核态的切换开销远超等待那几条指令的时间。

因此,CAS 失败后,线程进入 自适应自旋(Adaptive Spinning)

flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:2px,color:#ede9fe,font-weight:bold;
    CAS[CAS 尝试将 Mark Word\n指向自己的 Lock Record] -->|成功| ENTER[进入同步块]
    CAS -->|失败| CHECK{是否已经\n持有该锁?}
    CHECK -->|是 锁重入| REENTER[创建新Lock Record\ndisplaced_header=null]
    CHECK -->|否 其他线程持有| SPIN[自旋等待]
    SPIN --> SPIN_LOOP[CAS重试]
    SPIN_LOOP -->|成功| ENTER
    SPIN_LOOP -->|超时/自旋失败| INFLATE[锁膨胀\n→ 重量级锁]

class CHECK condition;
class CAS,ENTER,INFLATE,REENTER,SPIN,SPIN_LOOP process;

自适应自旋的决策逻辑

这次自旋的决策依据说明
同一把锁的上一次自旋结果上次自旋成功了 → 这次多自旋一会(JVM 推测这次也能等到);上次自旋失败了 → 这次少自旋,甚至直接膨胀
持有锁的线程是否在运行持有锁的线程正在 CPU 上运行 → 大概率很快释放,值得自旋;持有锁的线程被阻塞/挂起 → 不可能马上释放,直接膨胀

🔓 5.4 解锁过程

轻量级锁的解锁也是通过 CAS:

  1. 从当前线程的 Lock Record 中取出 displaced_mark_word(之前保存的原始 Mark Word)
  2. 通过 CAS 将锁对象的 Mark Word 恢复为 displaced_mark_word
  3. 如果 CAS 成功:解锁完成
  4. 如果 CAS 失败:说明锁已经膨胀为重量级锁(其他线程把 Mark Word 改成了指向 ObjectMonitor 的指针),走重量级锁的释放路径(ObjectSynchronizer::slow_exit
// 轻量级锁解锁(概念层面)
void lightweightUnlock(Object obj) {
    BasicLock lockRecord = currentThread().popLockRecord();

    if (lockRecord.getDisplacedHeader() == null) {
        // 这是重入的 Lock Record,直接弹出即可
        return;
    }

    // CAS 恢复原始 Mark Word
    if (CAS(&obj.markWord,
            encodePtr(lockRecord) | 0b00,      // 期望值:仍指向这个LockRecord
            lockRecord.getDisplacedHeader())) { // 新值:恢复为无锁状态
        // CAS 成功
        return;
    }

    // CAS 失败 → 已膨胀 → 走重量级锁释放
    heavyweightUnlock(obj);
}

🧪 5.5 轻量级锁的 JOL 验证

轻量级锁最难验证,因为它需要"两个线程交替执行"的精确时机。下面用 CountDownLatch 精确控制:

public class LightweightLockDemo {
    public static void main(String[] args) throws Exception {
        Object lock = new Object();
        CountDownLatch t1Acquired = new CountDownLatch(1);
        CountDownLatch t1Wait = new CountDownLatch(1);

        // T1: 先拿锁,等T2来竞争
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                t1Acquired.countDown(); // 通知T2可以来了
                try { t1Wait.await(); } catch (Exception e) {}
                // T1不立即退出,让T2自旋等待
            }
        });

        // T2: 竞争同一把锁
        Thread t2 = new Thread(() -> {
            try { t1Acquired.await(); } catch (Exception e) {}
            // T1还持有锁,T2 CAS会失败 → 自旋
            synchronized (lock) {
                System.out.println("=== T2获得锁后(此时是轻量级锁) ===");
                System.out.println(ClassLayout.parseInstance(lock).toPrintable());
                // Mark Word低2位=00 → 轻量级锁确认
            }
        });

        t1.start();
        t2.start();
        t1.await(); // 等T1退出
        t2.join();
    }
}

输出:Mark Word 低 2 位为 00,确认为轻量级锁。

6️⃣ 六、重量级锁(Heavyweight Lock)

📌 6.1 什么时候升级到重量级锁

以下任意条件满足时,轻量级锁膨胀为重量级锁:

触发条件说明
自旋超时线程自旋等待轻量级锁期间,CAS 重试达到阈值仍然失败,不再继续自旋
等待队列长度 ≥ 1当有第 2 个线程在等待同一把锁时(即总共 3 个线程争抢),JVM 判断竞争激烈,直接膨胀
线程调用了 wait()wait() 依赖 ObjectMonitor 的 WaitSet,必须使用重量级锁
锁重入次数过多轻量级锁的重入通过栈上的 Lock Record 数量来体现,栈深度有上限

🔍 6.2 ObjectMonitor 的内部结构

重量级锁的数据结构是 ObjectMonitor ,分配在 Native Memory(C++ 堆,不在 Java 堆中)。它不是 Java 对象,不受 GC 管理。

flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef data fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
classDef root fill:#0f172a,stroke:#3b82f6,stroke-width:2.5px,color:#bfdbfe,font-weight:bold;
classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2.5px,color:#fce7f3,font-weight:bold;
    subgraph HEAP[堆]
        OBJ[Java对象 lock]
        MW["Mark Word\nptr_to_ObjectMonitor\nlock=10"]
    end
    subgraph NATIVE[Native Memory]
        OM["ObjectMonitor"]
        subgraph OWNER[线程控制]
            OM_OWNER["_owner: Thread*\n持有锁的线程"]
            OM_REC["_recursions: intptr_t\n重入计数"]
            OM_SUCC["_succ: Thread*\n继承者线程"]
        end
        subgraph QUEUE[队列管理]
            OM_CXQ["_cxq: ObjectWaiter*\n竞争队列头指针"]
            OM_ELIST["_EntryList: ObjectWaiter*\nBLOCKED 等待获取锁"]
            OM_WSET["_WaitSet: ObjectWaiter*\nWAITING wait()"]
        end
        OM_CNT["_count: intptr_t\n竞争计数近似值"]
    end
    MW --> OM
    OM --> OWNER
    OM --> QUEUE
    OM --> OM_CNT

class HEAP,OM_CXQ,QUEUE data;
class MW,NATIVE,OBJ,OM,OM_CNT,OM_OWNER,OM_REC,OM_SUCC,OM_WSET,OWNER process;
class OM_ELIST root;
class wait startEnd;

关键字段说明(HotSpot 源码 objectMonitor.hpp):

字段类型说明
_ownerThread*当前持有锁的线程指针。null 表示锁未被持有。这是重量级锁的"锁标记"
_recursionsintptr_t锁重入次数。同一个线程每进入一次 synchronized 块就 +1,退出时 -1。减到 0 时释放锁
_EntryListObjectWaiter*等待获取锁的线程链表。线程在这里处于BLOCKED 状态
_WaitSetObjectWaiter*调用了 wait() 的线程链表。线程在这里处于 WAITING 状态
_cxqObjectWaiter*竞争队列。新到达的竞争线程先放入 _cxq,释放锁时再移到 _EntryList

_cxq_EntryList 是两个队列。新的竞争线程先入 _cxq,锁释放时 _cxq 中的线程被转移到 _EntryList,然后从 _EntryList 头部取出一个线程唤醒。双队列的设计减少了锁释放时对 _EntryList 的并发操作——新竞争者只操作 _cxq,释放者操作 _EntryList

📈 6.3 从膨胀到加锁的完整序列

锁膨胀(Inflation)和随后的加锁过程,画在一个时序图中:

sequenceDiagram
    participant T1 as 线程T1\n已持有轻量级锁
    participant T2 as 线程T2\nCAS竞争失败
    participant MW as Mark Word\n(堆中对象)
    participant OM as ObjectMonitor\n(Native Memory)

    T2->>MW: CAS写入自己的Lock Record指针
    MW->>T2: CAS失败!(已被T1的Lock Record占据)
    T2->>T2: 开始自旋
    Note over T2: 自旋超时或放弃

    T2->>OM: 调用inflate()\n创建/获取ObjectMonitor
    OM->>OM: _owner = T1
    OM->>OM: T2包装为ObjectWaiter\n加入_cxq队列

    T2->>MW: CAS(&mark_word,\n旧=ptr_to_LockRecord, 新=ptr_to_OM | 10)
    Note over MW: Mark Word变为\n┌ ptr_to_OM ┐ 10\nlock位=10

    T2->>OM: pthread_mutex_lock()\n挂起,状态→BLOCKED

    T1->>MW: 尝试释放轻量级锁\nCAS恢复旧Mark Word
    MW->>T1: CAS失败!(Mark Word已指向OM)
    T1->>OM: 走heavyweight_unlock路径

    OM->>OM: _recursions == 0\n_owner = null
    OM->>OM: 从_EntryList取出T2\n移入_cxq→_EntryList
    OM->>OM: pthread_mutex_unlock()\n唤醒T2
    OM->>OM: _owner = T2
    Note over T2: 状态→RUNNABLE\n获得重量级锁

关键细节:<span style="color:red">膨胀过程中,T1 仍然持有锁(先通过轻量级锁,膨胀后 Mark Word 指向 OM,OM._owner = T1)。膨胀不影响 T1 的执行——T1 甚至不知道自己持有的锁已经被膨胀了。只有 T1 尝试退出同步块时,才会发现 CAS 恢复 Mark Word 失败,转而走重量级锁的退出路径。

🔍 6.4 wait / notify 与 ObjectMonitor 的协作

wait()notify() 是重量级锁独有功能——它们直接操作 ObjectMonitor 的 _WaitSet_EntryList

sequenceDiagram
    participant T1 as 线程T1\n持有锁
    participant OM as ObjectMonitor
    participant WS as _WaitSet
    participant EL as _EntryList
    participant T2 as 线程T2\n被唤醒

    T1->>OM: lock.wait()
    OM->>WS: 将T1包装为ObjectWaiter\n加入_WaitSet
    OM->>OM: _owner = null\n_recursions = 0
    OM->>EL: 唤醒_EntryList中下一个线程\n(如果有的话)
    Note over T1: 状态→WAITING\n等待notify唤醒

    Note over T2: 另一个线程调用\nlock.notify()
    T2->>WS: 从_WaitSet取出一个线程
    WS->>EL: 移动到_EntryList
    Note over T1: 状态→BLOCKED\n等待重新获取锁

    OM->>T1: T1被调度为_owner后\n从wait()返回\n重新获取到锁

wait() 的三个关键步骤:

  1. 当前线程 必须持有锁_owner == current_thread),否则抛出 IllegalMonitorStateException
  2. 线程被包装为 ObjectWaiter 放入 _WaitSet
  3. 释放锁(_owner = null,唤醒 _EntryList 中的等待者),当前线程挂起

notify() 的关键步骤:

  1. 当前线程 必须持有锁 ,否则抛异常
  2. _WaitSet 中取出一个线程(通常是队头,不保证公平)
  3. 将其从 _WaitSet 移到 _EntryList(状态从 WAITING 变为 BLOCKED)
  4. 被通知的线程不会立即运行 ——它仍然需要等待操作系统将锁分配给它

这也是为什么 wait() 醒来后必须重新检查条件——从 wait() 返回到线程真正持有锁并继续执行之间,可能有其他线程已经改变了条件。

🔄 七、完整流程总结

📊 7.1 所有锁状态及其 Mark Word 结构的并列对比

将五种状态放在一张图中,可以清晰看到同一块 64 位内存在不同状态下的复用情况:

flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
    subgraph S1["无锁 biased_lock=0 lock=01"]
        S1A["unused:25"]
        S1B["identity_hashcode:31"]
        S1C["unused:1"]
        S1D["age:4"]
        S1E["0"]
        S1F["01"]
    end
    subgraph S2["偏向锁 biased_lock=1 lock=01"]
        S2A["ThreadID:54"]
        S2B["epoch:2"]
        S2C["unused:1"]
        S2D["age:4"]
        S2E["1"]
        S2F["01"]
    end
    subgraph S3["轻量级锁 lock=00"]
        S3A["ptr_to_LockRecord:62"]
        S3B["00"]
    end
    subgraph S4["重量级锁 lock=10"]
        S4A["ptr_to_ObjectMonitor:62"]
        S4B["10"]
    end
    subgraph S5["GC标记 lock=11"]
        S5A["forwarding_ptr:62"]
        S5B["11"]
    end

class S1,S1A,S1B,S1C,S1D,S1E,S1F,S2,S2A,S2B,S2C,S2D,S2E,S2F,S3,S3A,S3B,S4,S4A,S4B,S5,S5A,S5B process;

⚙️ 7.2 锁升级的判断逻辑(JVM 核心决策树)

flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:2px,color:#ede9fe,font-weight:bold;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2.5px,color:#fce7f3,font-weight:bold;
    ENTER[synchronized进入] --> READ[读取Mark Word低3位]
    READ --> BITS{低3位 = ?}

    BITS -->|"001 无锁"| CHECK_BIAS{偏向锁是否启用?}
    CHECK_BIAS -->|是| DO_BIAS[CAS写入ThreadID\n→ 偏向锁]
    CHECK_BIAS -->|否| DO_LIGHT[CAS写入LockRecord指针\n→ 轻量级锁]

    BITS -->|"101 偏向锁"| CHECK_THREAD{ThreadID == 当前线程?}
    CHECK_THREAD -->|是| ENTERED[直接进入同步块]
    CHECK_THREAD -->|否| REVOKE[撤销偏向锁\n→ 升级为轻量级锁]

    BITS -->|"00 轻量级锁"| LIGHT_CAS{CAS写入自己的\nLockRecord指针}
    LIGHT_CAS -->|成功| ENTERED
    LIGHT_CAS -->|失败| LIGHT_CHECK{持有者是当前线程?}
    LIGHT_CHECK -->|是 锁重入| ENTERED
    LIGHT_CHECK -->|否| SPIN_DECIDE[自旋或膨胀]

    BITS -->|"10 重量级锁"| HEAVY_ENTER[操作ObjectMonitor]
    HEAVY_ENTER --> OWNER_CHECK{_owner == 当前线程?}
    OWNER_CHECK -->|是 锁重入| INCREC[_recursions++]
    OWNER_CHECK -->|否| OS_LOCK[pthread_mutex_lock\n挂起等待]

    BITS -->|"11 GC标记"| GC_WAIT[等待GC完成\n重读Mark Word]

class BITS,CHECK_BIAS,CHECK_THREAD,LIGHT_CAS,LIGHT_CHECK,OWNER_CHECK condition;
class DO_BIAS,DO_LIGHT,ENTER,ENTERED,HEAVY_ENTER,INCREC,OS_LOCK,READ,REVOKE,SPIN_DECIDE process;
class GC_WAIT startEnd;

🔢 7.3 锁状态转换总表

当前状态Mark Word 低3位触发事件下一状态Mark Word 新值
无锁001第一个线程获取锁(偏向启用)偏向锁101 + ThreadID
无锁001第一个线程获取锁(偏向禁用)轻量级锁00 + ptr_to_LockRecord
偏向锁101同一线程再次进入偏向锁(不变)不变
偏向锁101另一线程竞争 → 撤销轻量级锁00 + ptr_to_LockRecord
轻量级锁00CAS 成功(获得锁)轻量级锁(不变)不变(已指向自己的 Lock Record)
轻量级锁00CAS 失败 + 自旋超时 / 竞争数 > 1重量级锁10 + ptr_to_ObjectMonitor
轻量级锁00wait() 被调用重量级锁10 + ptr_to_ObjectMonitor
重量级锁10重量级锁(不变)不变(不会降级)

八、日常开发中的 synchronized 用法

🎯 8.1 三种形式与选型

形式锁对象适合场景注意
synchronized void method()this(当前实例)保护该实例的状态子类与父类共享同一把 this
static synchronized void method()ClassName.class保护静态状态 / 全局资源与实例方法的锁不同,互不影响
synchronized (lockObject) {}显式指定对象精确控制锁粒度推荐:专用 private final 锁对象
public class SynchronizedUsage {
    private int instanceVar = 0;
    private static int staticVar = 0;
    // 专用锁对象,不暴露给外部
    private final Object lock = new Object();

    // 实例方法——锁this
    public synchronized void incrementInstance() {
        instanceVar++;
    }

    // 静态方法——锁SynchronizedUsage.class
    public static synchronized void incrementStatic() {
        staticVar++;
    }

    // 代码块——锁指定对象,粒度更细
    public void incrementWithBlock() {
        // ... 非同步代码(可以并发执行)
        synchronized (lock) {
            instanceVar++;
        }
        // ... 非同步代码
    }
}

<span style="color:red">优先使用同步代码块而非同步方法:同步代码块可以控制锁的粒度——只锁住需要保护的代码,让不需要同步的逻辑并发执行。同步方法把整个方法体都锁住,粒度太粗。

🛠️ 8.2 wait / notify 的规范用法

方法用途调用前提后果
obj.wait()释放锁,等待通知必须持有 obj 的锁线程进入 WAITING,释放锁
obj.wait(long ms)带超时的等待同上超时后自动唤醒,重新竞争锁
obj.notify()随机唤醒 _WaitSet 中一个线程必须持有 obj 的锁被唤醒线程从 WAITING→BLOCKED
obj.notifyAll()唤醒 _WaitSet 中所有线程同上所有等待线程进入 BLOCKED,竞争锁
// 生产者-消费者:规范的 wait/notify 用法
class BoundedBuffer<T> {
    private final T[] buffer;
    private int count = 0;
    private int putIndex = 0;
    private int takeIndex = 0;

    public BoundedBuffer(int capacity) {
        buffer = (T[]) new Object[capacity];
    }

    public synchronized void put(T item) throws InterruptedException {
        while (count == buffer.length) {   // 必须用while,不能用if!
            wait();                         // 等待非满
        }
        buffer[putIndex] = item;
        putIndex = (putIndex + 1) % buffer.length;
        count++;
        notifyAll();                        // 通知等待的消费者
    }

    public synchronized T take() throws InterruptedException {
        while (count == 0) {               // 必须用while,不能用if!
            wait();                         // 等待非空
        }
        T item = buffer[takeIndex];
        buffer[takeIndex] = null;
        takeIndex = (takeIndex + 1) % buffer.length;
        count--;
        notifyAll();                        // 通知等待的生产者
        return item;
    }
}

<span style="color:red">核心规则:wait() 必须在 while 循环中调用,禁止用 if。两个原因:

  1. 虚假唤醒(Spurious Wakeup) :操作系统可能在没有线程调用 notify() 的情况下唤醒等待线程。这是 POSIX 规范允许的行为,JVM 不能禁止。
  2. 条件被抢先修改 :从线程被 notify 唤醒到它真正拿到锁并继续执行之间,可能有另一个线程抢先拿到锁并修改了条件。用 while 重新检查条件可以防御这种情况。

错误写法:

// 错误——线程被虚假唤醒后就继续执行
if (count == buffer.length) {
    wait();
}

❌ 8.3 常见错误与排查

错误场景症状根因修正
锁对象被重新赋值同步失效,多线程同时进入临界区lock = new Object() 改变了锁引用锁对象声明为 final
锁 String 常量全局死锁或性能崩溃String 常量池导致不同模块共享同一把锁使用 new Object()
wait() 不在 synchronized 内IllegalMonitorStateExceptionwait() 要求持有锁wait() 放在 synchronized(obj) 块内
notifyAll() 误写为 notify()某些线程永不唤醒notify() 只唤醒一个,其他线程饿死不确定唤醒哪个线程时用 notifyAll()
同步方法嵌套死锁锁顺序不一致统一锁获取顺序,或用 ReentrantLock.tryLock()
锁对象不 final不同线程锁住不同对象lock = newXxx() 后引用变了private final Object lock = new Object()

🎯 8.4 synchronized vs ReentrantLock 的选型

维度synchronizedReentrantLock
释放方式自动(退出同步块/JVM 异常处理)必须手动 finally { unlock() }
公平锁不支持(非公平)支持 new ReentrantLock(true)
可中断获取不支持lockInterruptibly()
尝试获取不支持tryLock() / tryLock(time, unit)
多条件变量不支持(一个对象一个等待队列)支持 newCondition(),一个锁多个 Condition
性能(JDK 6+)锁升级后与 ReentrantLock 基本持平持平
适用场景默认选择——代码简洁、自动释放需要公平锁 / 可中断 / 尝试获取 / 多条件时

选型建议:<span style="color:red">默认用 synchronized,只有在需要 tryLocklockInterruptibly、公平锁或多条件变量时才用 ReentrantLock。

🎯 九、总结

synchronized 的锁升级机制是 JDK 6 对 Java 并发性能最重要的优化之一。它的核心逻辑只有三个判断:

  1. 读 Mark Word 的低 3 位 ——一条位与指令确定当前锁状态
  2. 根据锁状态走对应分支 ——偏向锁比较 ThreadID(无 CAS)、轻量级锁 CAS 写 LockRecord 指针、重量级锁操作 ObjectMonitor
  3. 竞争发生时升级 ——偏向→撤销→轻量级锁→自旋→膨胀→重量级锁

每一级锁的选择都对应一个硬件开销的权衡:

硬件开销设计权衡
偏向锁一次比较指令用 Mark Word 的空间(54 位存 ThreadID)换取免 CAS 的快速加锁
轻量级锁CAS + 栈空间(Lock Record)用栈空间 + 自旋 CPU 时间换取免 OS 互斥锁
重量级锁OS Mutex + 上下文切换用线程阻塞换取 CPU 不被自旋空转浪费

Mark Word 中那 3 个比特位是这一切的开关——JVM 通过读取它们,在几纳秒内决定走哪条路径。理解了 Mark Word 的状态转换,就理解了 synchronized 的全部机制。