synchronized 是 Java 最基础的同步机制,它的原理横跨 字节码 → JVM 运行时 → 操作系统内核 三个层面。

一、字节码层面:monitorenter 与 monitorexit

编译器对 synchronized 的处理有两种形式:

1. 同步代码块

public void syncBlock() {
    synchronized (this) {
        // 临界区
    }
}

编译后字节码:

 0: aload_0
 1: dup
 2: astore_1          // 将对象引用存入局部变量
 3: monitorenter      // 【进入】获取对象监视器锁
 4: // 临界区代码...
11: aload_1
12: monitorexit       // 【正常退出】释放锁
13: goto 21
16: astore_2          // 异常处理开始
17: aload_1
18: monitorexit       // 【异常退出】释放锁(保证异常时也能释放)
19: aload_2
20: athrow
21: return

关键发现:即使代码中只有 1 个 synchronized 块,字节码里也会出现 两个 monitorexit —— 一个用于正常路径,一个用于异常路径。这是 JVM 保证锁一定会被释放的机制。

2. 同步方法

public synchronized void syncMethod() { }

编译后不会在方法体里插入 monitorenter/monitorexit,而是在方法的 访问标志(Access Flags) 中设置 ACC_SYNCHRONIZED(0x0020)。JVM 调用方法时会检查该标志,自动进行加锁/解锁。

二、对象头:锁状态的存储载体

synchronized 锁的所有状态信息都存储在对象的 对象头(Object Header) 中。
对象头结构(64 位 JVM,开启压缩指针)

┌─────────────────────────────────────────────────────────────┐
│                    Object Header (128 bits)                │
├─────────────────────────────┬───────────────────────────────┤
│      Mark Word (64 bits)    │   Class Metadata Address (64) │
│   (存储锁状态、GC年龄、哈希码)  │   (指向 Klass 对象的指针)      │
└─────────────────────────────┴───────────────────────────────┘

Mark Word 的锁状态变化

锁状态 62 bits 内容 biased_lock lock
无锁 hash:31 + age:4 + ... 0 01
偏向锁 thread:54 + epoch:2 + age:4 1 101
轻量级锁 ptr_to_lock_record:62 - 00
重量级锁 ptr_to_monitor:62 - 10
GC 标记 - - 11

lock(2 bits)+ biased_lock(1 bit)共同决定当前对象的锁状态。

三、锁升级:synchronized 的核心优化

JDK 1.6 之前,synchronized 直接调用操作系统 mutex,性能极差。JDK 1.6 引入了 锁升级体系,根据竞争程度动态调整:

3.1 无锁 → 偏向锁

场景:只有一个线程反复进入同步块。
原理

  1. 线程第一次获取锁时,用 CAS 将自己的 线程 ID 写入 Mark Word
  2. 之后该线程再次进入,只需检查 Mark Word 中的线程 ID 是否等于自己
  3. 相等则直接通过,无需任何原子操作,性能接近无锁

撤销

  • 当另一个线程尝试获取该锁时,JVM 需要在 安全点(STW) 撤销偏向锁
  • 撤销成本较高,这也是 JDK 15 废弃偏向锁的原因

3.2 偏向锁 → 轻量级锁

场景:多线程交替执行(没有同时竞争)。
原理

  1. 线程在自己的栈帧中创建 Lock Record(锁记录)
  2. 用 CAS 将对象头的 Mark Word 替换为指向该 Lock Record 的指针
    • 成功:获得轻量级锁
    • 失败:说明有其他线程在竞争,进入自旋

自旋

  • 线程在 CPU 上空转(循环检查锁是否释放),默认 10 次
  • JDK 1.6 引入 自适应自旋:根据历史成功率动态调整自旋次数
  • 自旋避免了线程阻塞/唤醒的内核态切换开销,但消耗 CPU

3.3 轻量级锁 → 重量级锁

场景:竞争激烈,自旋失败。
原理锁膨胀(inflate) 为 ObjectMonitor。

对象头 Mark Word 原来指向 Lock Record
                ↓
        修改为指向 ObjectMonitor 地址
                ↓
    未获得锁的线程进入 _EntryList 阻塞(挂起)

膨胀过程

  1. 检查 Mark Word 是否已指向 Monitor
  2. 如果没有,在堆中分配一个 ObjectMonitor 对象
  3. 用 CAS 将 Mark Word 改为指向 Monitor 的指针
  4. 竞争失败的线程调用 pthread_mutex_lock 进入内核态阻塞

四、重量级锁的底层:ObjectMonitor

当锁膨胀为重量级锁后,JVM 使用 ObjectMonitor(C++ 实现)管理线程排队和唤醒。
ObjectMonitor 核心字段

// hotspot/share/runtime/objectMonitor.hpp
class ObjectMonitor {
    volatile markOop   _header;       // 保存原始 Mark Word
    void*              _object;       // 指向锁对象
    volatile int       _count;        // 重入次数
    volatile int       _waiters;      // 调用 wait 的线程数
    volatile int       _recursions;   // 锁重入次数
    ObjectWaiter*      _EntryList;    // 【阻塞队列】竞争锁失败的线程
    ObjectWaiter*      _WaitSet;      // 【等待队列】调用 wait() 的线程
    Thread* volatile   _owner;        // 当前持有锁的线程
    volatile int       _cxq;          // Contention List(竞争列表)
};

获取锁的流程(monitorenter)

线程尝试获取锁
    │
    ▼
检查 _owner 是否为空
    │
    ├── 为空 ──→ CAS 设置 _owner = 当前线程 ──→ 获取成功
    │
    └── 不为空 ──→ 检查 _owner 是否是当前线程 ──→ 是:_recursions++(重入)
                              │
                              └── 否:进入 _EntryList 阻塞(挂起)

释放锁的流程(monitorexit)

持有锁的线程执行完毕
    │
    ▼
_recursions--(处理重入)
    │
    ▼
_recursions == 0 ?
    │
    ├── 否:继续执行
    │
    └── 是:释放锁
            │
            ▼
    从 _EntryList 或 _cxq 中唤醒一个线程
            │
            ▼
    被唤醒的线程重新竞争锁(非公平)

五、wait/notify 的底层实现

wait() 和 notify() 必须在 synchronized 块内调用,因为它们依赖 ObjectMonitor。
wait() 执行流程

void ObjectMonitor::wait(TRAPS) {
    // 1. 检查当前线程是否持有锁
    CHECK_OWNER();
    
    // 2. 创建 ObjectWaiter 节点
    ObjectWaiter node(THREAD);
    
    // 3. 将当前线程加入 _WaitSet(等待队列)
    AddWaiter(&node);
    
    // 4. 释放锁(_owner = NULL, _recursions = 0)
    exit(true, TRAPS);
    
    // 5. 线程挂起(Park)
    node._event->park();
    
    // 6. 被 notify 唤醒后,重新竞争锁
    enter(TRAPS);
}

关键点:

  • wait() 会释放锁,然后线程进入 _WaitSet 挂起
  • 被唤醒后,线程需要重新竞争锁(从 _WaitSet 移到 _EntryList 或直接竞争)

notify() 执行流程

void ObjectMonitor::notify(TRAPS) {
    // 1. 检查当前线程是否持有锁
    CHECK_OWNER();
    
    // 2. 从 _WaitSet 中取出一个线程
    ObjectWaiter* iterator = DequeueWaiter();
    
    // 3. 将该线程移到 _EntryList(或 _cxq)
    // 被唤醒的线程不会立即执行,需要等待当前线程释放锁后竞争
    iterator->TState = ObjectWaiter::TS_ENTER;
    iterator->notify_enter(this);
}

关键点

  • notify() 不会立即释放锁,只是将 _WaitSet 中的一个线程移到竞争队列
  • 被唤醒的线程必须等当前线程执行完 synchronized 块释放锁后,才能参与竞争

六、锁消除与锁粗化(编译器优化)

除了运行时的锁升级,JVM 编译器还会做两项优化:

6.1 锁消除(Lock Elimination)

原理:逃逸分析发现锁对象不会被其他线程访问,直接去掉同步

public void method() {
    StringBuffer sb = new StringBuffer();  // 局部变量,不会逃逸
    sb.append("a");  // StringBuffer 的方法都是 synchronized
    sb.append("b");  // 编译器会消除这些锁
}

6.2 锁粗化(Lock Coarsening)

原理:相邻的同步块使用同一个锁,合并为一个更大的同步块。

// 优化前
synchronized(lock) { doSomething(); }
synchronized(lock) { doSomethingElse(); }

// 优化后
synchronized(lock) {
    doSomething();
    doSomethingElse();
}

七、完整流程图

┌─────────────────────────────────────────────────────────────┐
│                     synchronized 完整原理                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  源代码层                                                    │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ synchronized(obj) { ... }  /  synchronized method  │   │
│  └────────────────────┬────────────────────────────────┘   │
│                       │                                     │
│  字节码层              ▼                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 同步块: monitorenter + monitorexit(×2)             │   │
│  │ 同步方法: ACC_SYNCHRONIZED 标志位                   │   │
│  └────────────────────┬────────────────────────────────┘   │
│                       │                                     │
│  JVM 运行时层          ▼                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 检查对象头 Mark Word                                  │   │
│  │                                                      │   │
│  │  无锁 ──CAS──→ 偏向锁(记录线程ID)                    │   │
│  │    ↑                                                 │   │
│  │    │  线程ID匹配?是→ 直接通过                        │   │
│  │    │              否→ 撤销偏向锁                       │   │
│  │    │                      ↓                          │   │
│  │    │              轻量级锁(Lock Record + CAS)        │   │
│  │    │                      ↓                          │   │
│  │    │              自旋失败?是→ 锁膨胀                  │   │
│  │    │                      ↓                          │   │
│  │    └──────────── 重量级锁(ObjectMonitor)            │   │
│  │                         │                            │   │
│  │                         ▼                            │   │
│  │              ┌─────────────────────┐                 │   │
│  │              │   _owner = 当前线程  │                 │   │
│  │              │   竞争失败→ _EntryList │                 │   │
│  │              │   wait() → _WaitSet  │                 │   │
│  │              │   notify()→移入竞争队列│                 │   │
│  │              └─────────────────────┘                 │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  操作系统层                                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 重量级锁调用 pthread_mutex_lock / pthread_cond_wait  │   │
│  │ 线程阻塞 = 用户态→内核态切换(上下文切换)              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

八、总结

层面 核心机制
字节码 monitorenter/monitorexit(代码块)或 ACC_SYNCHRONIZED(方法)
对象头 Mark Word 存储锁状态(无锁/偏向/轻量/重量)
JVM 优化 锁升级(无锁→偏向→轻量→重量)、锁消除、锁粗化
重量级锁 ObjectMonitor 管理 _owner_EntryList_WaitSet
内核层 pthread_mutex 实现线程阻塞与唤醒

synchronized 在 JDK 1.6 之后已经不再是"性能差"的代名词。JVM 通过锁升级体系,让大多数场景下它的性能与 ReentrantLock 相当,甚至在无竞争场景下更优(偏向锁零开销)。不过随着偏向锁在 JDK 15 被废弃,未来的趋势是 轻量级锁 + CAS 成为 synchronized 的主流路径。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐