深入理解 Java synchronized:从 Monitor 到锁升级机制

前言

在学习 Java 高并发时,很多人都会接触到 synchronized,但往往停留在:

synchronized(lock) {
    // 临界区
}

或者:

public synchronized void method() {
}

知道它能保证线程安全,却不清楚 JVM 底层到底做了什么。

本文将从 Monitor、对象头、锁升级、WaitSet 等角度,系统梳理 synchronized 的实现原理。


一、synchronized 的本质

synchronized 本质上依赖 JVM 的 Monitor(监视器) 实现同步。

可以简单理解为:

Object
  ↓
Monitor
  ├── Owner
  ├── EntryList
  └── WaitSet

其中:

  • Owner:当前持有锁的线程
  • EntryList:等待获取锁的线程
  • WaitSet:调用 wait() 后等待被唤醒的线程

当线程进入:

synchronized(lock) {
}

实际上是在尝试获取对象关联的 Monitor。


二、同步代码块是如何实现的

代码:

synchronized(lock) {
    count++;
}

编译后会生成:

monitorenter

count++

monitorexit

执行流程:

monitorenter
    ↓
获取Monitor
    ↓
执行代码
    ↓
monitorexit
    ↓
释放Monitor

因此同步代码块是通过 JVM 字节码指令直接操作 Monitor。


三、同步方法是如何实现的

代码:

public synchronized void add() {
    count++;
}

反编译后不会看到:

monitorenter
monitorexit

而是:

ACC_SYNCHRONIZED

方法标记。

很多人误以为:

同步方法 ≠ Monitor

实际上这是错误的。


ACC_SYNCHRONIZED 的作用

JVM 在调用方法时发现:

ACC_SYNCHRONIZED

会自动执行:

获取Monitor
    ↓
执行方法
    ↓
释放Monitor

可以理解为:

if(method.isSynchronized()){
    monitor_enter();
}

invoke();

if(method.isSynchronized()){
    monitor_exit();
}

因此:

  • 同步代码块:显式使用 monitorenter/monitorexit
  • 同步方法:JVM隐式获取和释放 Monitor

底层最终都是 Monitor。


四、实例方法和静态方法锁的是什么

实例同步方法:

public synchronized void test() {
}

等价于:

public void test() {
    synchronized(this) {
    }
}

锁对象:

this

静态同步方法:

public static synchronized void test() {
}

等价于:

public static void test() {
    synchronized(Test.class) {
    }
}

锁对象:

Test.class

五、Monitor 的结构

一个 Monitor 可以抽象为:

Monitor
 ├── Owner
 ├── EntryList
 └── WaitSet

Owner

表示当前持有锁的线程。

例如:

Owner = ThreadA

表示 A 正在执行同步代码。


EntryList

存放竞争锁失败的线程。

例如:

ThreadA 获得锁

ThreadB 请求锁
ThreadC 请求锁
ThreadD 请求锁

结果:

Monitor
 ├── Owner = A
 ├── EntryList
 │    B
 │    C
 │    D
 └── WaitSet

这些线程会等待 A 释放锁。


WaitSet

WaitSet 与 EntryList 完全不同。

它存放的是:

主动调用 wait() 的线程

例如:

synchronized(lock){

    while(queue.isEmpty()){
        lock.wait();
    }

    consume();
}

执行:

lock.wait();

时会发生两件事:

1. 释放锁

Owner = null

2. 进入 WaitSet

Monitor
 ├── Owner = null
 ├── EntryList
 └── WaitSet
      A

六、notify() 的作用

生产者线程:

synchronized(lock){
    queue.add(item);
    lock.notify();
}

执行:

notify()

不会让线程立刻运行。

而是:

WaitSet
    ↓
EntryList

即:

Monitor
 ├── Owner = Producer
 ├── EntryList
 │    Consumer
 └── WaitSet

然后等待当前线程释放锁。

Consumer 再次竞争锁成功后:

wait() 返回
继续执行

七、为什么 wait() 必须在 synchronized 中调用

例如:

lock.wait();

如果当前线程没有持有锁。

JVM 根本不知道:

应该释放哪个Monitor?

因此直接抛出:

IllegalMonitorStateException

八、对象头与 Mark Word

HotSpot 对象结构:

Object
 ├── Mark Word
 ├── Klass Pointer
 └── Instance Data

其中 Mark Word 保存:

  • 锁状态
  • hashCode
  • GC年龄
  • 线程信息

等元数据。


锁升级的本质:

不断修改 Mark Word

中的内容。


九、锁升级机制

JDK8 时代:

无锁
 ↓
偏向锁
 ↓
轻量级锁
 ↓
重量级锁

1. 无锁状态

对象刚创建:

Object obj = new Object();

此时:

Mark Word
=
无锁

2. 偏向锁

第一次进入:

synchronized(obj)

线程A获得锁。

对象头记录:

ThreadId = A

以后 A 再进入:

发现还是自己

直接成功。

无需 CAS。


3. 轻量级锁

当线程B出现:

A 持有锁
B 也想获取

偏向锁被撤销。

升级:

偏向锁
 ↓
轻量级锁

轻量级锁依赖:

CAS

竞争。

失败不会立即阻塞。

而是:

自旋

不断尝试获取锁。


4. 自旋锁

线程B:

CAS失败
↓
继续CAS
↓
继续CAS

因为 JVM 认为:

A 很快会释放锁

直接阻塞反而更贵。


5. 重量级锁

如果竞争越来越激烈:

A
B
C
D
E

持续争抢同一把锁。

JVM 判断:

自旋成本过高

升级:

轻量级锁
 ↓
重量级锁

此时真正启用 Monitor。

线程进入:

BLOCKED

状态。


十、为什么偏向锁被移除了

JDK6 时期:

CAS 比较昂贵

偏向锁能够避免 CAS。

收益明显。


随着 CPU 和 JVM 优化:

CAS 越来越便宜

偏向锁的收益越来越小。


而偏向锁撤销却非常复杂:

发现竞争
 ↓
暂停线程
 ↓
撤销偏向
 ↓
升级轻量级锁

甚至可能触发:

Stop The World

现代应用:

  • 线程池
  • ForkJoinPool
  • 虚拟线程

线程切换频繁。

偏向锁适用场景越来越少。

因此:

  • JDK15 默认禁用偏向锁
  • 后续版本彻底移除

现代锁模型更接近:

无锁
 ↓
轻量级锁
 ↓
重量级锁

十一、一张图理解整个流程

                synchronized
                       │
                       ▼
                  获取Monitor
                       │
        ┌──────────────┴──────────────┐
        ▼                             ▼

    无竞争                       存在竞争

        │                             │
        ▼                             ▼

    轻量级锁                   CAS失败自旋

                                      │
                                      ▼

                                持续竞争

                                      │
                                      ▼

                                重量级锁

                                      │
                                      ▼

                              Monitor结构

                ┌─────────────────────┐
                │      Owner          │
                └─────────────────────┘
                          │

            ┌─────────────┴─────────────┐
            ▼                           ▼

        EntryList                  WaitSet

      抢锁失败线程              wait()线程

总结

synchronized 的核心并不是关键字本身,而是 JVM 提供的 Monitor 机制。

理解它可以抓住以下几个关键点:

  1. synchronized 最终都依赖 Monitor。
  2. 同步代码块使用 monitorenter/monitorexit。
  3. 同步方法使用 ACC_SYNCHRONIZED 标记。
  4. EntryList 保存抢锁失败线程。
  5. WaitSet 保存主动 wait() 的线程。
  6. 锁升级本质是 Mark Word 状态变化。
  7. 现代 JDK 已经移除了偏向锁。
  8. 当前主流模型是轻量级锁 + 自旋 + 重量级锁。

掌握这些内容后,再学习 AQS、ReentrantLock、线程池和并发容器时,会轻松很多。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐