在 Java 并发编程中,synchronized 是最基础、最常用的同步手段,而其背后依赖的核心概念就是 监视器(Monitor)。很多开发者会用 synchronized,但对 Monitor 是什么?JVM 如何实现它?wait/notify 如何协作? 却一知半解。本文将从底层原理出发,系统讲解这三大核心知识点。


一、监视器(Monitor):Java 并发的基石

1. 什么是 Monitor?

Monitor(监视器) 是一种 同步原语,由 C.A.R. Hoare 在 1974 年提出,用于解决多线程对共享资源的互斥访问和线程协作问题。

核心思想

  • 同一时刻,只有一个线程能进入 Monitor 的临界区
  • 线程可以在 Monitor 内 等待某个条件成立,并被其他线程唤醒。

2. Java 中的 Monitor 模型

在 JVM 中,每个 Java 对象都关联一个 Monitor(也叫“内置锁”或“Intrinsic Lock”)。该 Monitor 包含三个关键组件:

组件 作用
Owner 当前持有锁的线程
Entry Set(入口队列) 等待获取锁的线程队列(阻塞状态)
Wait Set(等待队列) 调用 wait() 后进入等待的线程队列
  • 当线程执行 synchronized(obj) 时,尝试获取 obj 的 Monitor;
  • 若 Monitor 未被占用,则线程成为 Owner;
  • 若已被占用,则进入 Entry Set 阻塞;
  • 若线程在同步块内调用 obj.wait(),则:
    • 释放 Monitor;
    • 进入 Wait Set;
    • 等待被 notify() 唤醒后重新竞争锁。

🔑 关键点
Monitor 是对象级别的,不是方法或代码块级别的synchronized 只是语法糖,真正锁的是对象。


二、synchronized 的底层实现原理

synchronized 在字节码层面由 monitorentermonitorexit 指令实现。

1. 字节码示例

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

编译后字节码:

public method()V
   L0
    aload_0
    dup
    astore_1
    monitorenter        // 获取 this 的 Monitor
   L1
    // 临界区代码
   L2
    aload_1
    monitorexit         // 释放 Monitor
   L3
    goto L4
   L5
    astore_2
    aload_1
    monitorexit         // 异常时也释放锁
    aload_2
    athrow
   L4

⚠️ 注意:JVM 会生成 两个 monitorexit,确保正常和异常路径都能释放锁。

2. JVM 层面的锁优化(JDK 6+)

早期 synchronized 性能差,因为直接依赖操作系统的 Mutex Lock(互斥锁),涉及用户态/内核态切换。JDK 6 引入了 锁升级机制

锁状态演进(从低到高):
锁类型 适用场景 实现方式
无锁 无竞争 对象头 Mark Word 正常
偏向锁 单线程反复加锁 Mark Word 存储线程 ID,无需 CAS
轻量级锁 多线程交替执行 CAS 自旋 + 栈帧 Lock Record
重量级锁 多线程竞争激烈 转为 OS Mutex,线程阻塞

优势
大部分场景下无需进入重量级锁,极大提升性能。

对象头 Mark Word 结构(32 位 JVM 示例):
锁状态 Mark Word 内容
无锁 hashcode | age
偏向锁 threadID | epoch | age | biased_bit
轻量级锁 指向栈中 Lock Record 的指针
重量级锁 指向 Monitor 对象的指针

💡 Monitor 对象:当升级为重量级锁时,JVM 会在堆中创建一个 ObjectMonitor 实例,并将对象头指向它。


三、线程间通信机制:wait / notify / notifyAll

synchronized 解决了 互斥,但线程协作需要 条件等待,这就是 wait()/notify() 的作用。

1. 通信流程(生产者-消费者模型)

private final Object lock = new Object();
private Queue<String> queue = new LinkedList<>();
private static final int MAX = 10;

// 生产者
public void produce(String item) throws InterruptedException {
    synchronized (lock) {
        while (queue.size() == MAX) {
            lock.wait(); // 队列满,等待
        }
        queue.add(item);
        lock.notifyAll(); // 通知消费者
    }
}

// 消费者
public String consume() throws InterruptedException {
    synchronized (lock) {
        while (queue.isEmpty()) {
            lock.wait(); // 队列空,等待
        }
        String item = queue.poll();
        lock.notifyAll(); // 通知生产者
        return item;
    }
}

2. wait() / notify() 工作原理

方法 行为
wait() 1. 释放当前对象的 Monitor
2. 将当前线程加入 Wait Set
3. 阻塞,直到被 notify 或中断
notify() 从 Wait Set 中随机唤醒一个线程,移入 Entry Set
notifyAll() 唤醒 Wait Set 中所有线程,全部进入 Entry Set

⚠️ 必须在 synchronized 块中调用!否则抛 IllegalMonitorStateException

3. 为什么用 while 而不是 if?

防止 虚假唤醒(Spurious Wakeup) —— 操作系统可能在无 notify 的情况下唤醒线程。

// 错误写法
if (queue.isEmpty()) {
    lock.wait(); // 被唤醒后可能仍为空!
}

// 正确写法
while (queue.isEmpty()) {
    lock.wait(); // 唤醒后重新检查条件
}

四、总结:三大机制的关系图

Java 对象
   │
   └── 关联一个 Monitor(重量级锁时)
        ├── Owner:当前持有锁的线程
        ├── Entry Set:等待获取锁的线程(synchronized 竞争)
        └── Wait Set:调用 wait() 的线程(线程协作)
                ↑
                └── 通过 notify()/notifyAll() 唤醒
  • synchronized → 控制 Entry Set,实现互斥;
  • wait/notify → 操作 Wait Set,实现协作;
  • Monitor → 作为底层容器,统一管理两者。

💬 面试金句
synchronized 是门禁系统,只允许一人进入;wait/notify 是对讲机,让里面的人可以喊话通知外面的人进来。而 Monitor,就是整栋大楼的安保中心。”


视频看了几百小时还迷糊?关注我,几分钟让你秒懂!(发点评论可以给博主加热度哦)

Logo

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

更多推荐