JUC 中 synchronized 的底层实现原理解析——Monitor

一、Monitor 核心概念

1.1 定义与核心作用

  • 核心定义 :Monitor(中文译为 "管程")是一种同步机制,本质是对象的同步工具,用于保证同一时间仅有一个线程能访问共享资源的临界区,解决多线程并发下的竞态条件问题。
  • 核心目标
    1. 互斥性:限制临界区同一时间只能被一个线程执行(解决指令交错)。
    2. 协作性:支持线程间通过等待 / 通知机制(wait/notify)实现高效协作(如生产者 - 消费者模型)。
  • 生活类比:Monitor 如同一个带锁的房间,临界区代码是房间内的资源,线程需先获取锁(钥匙)才能进入,房间内同一时间只能有一个人(线程),线程完成后释放锁,其他等待线程再竞争进入。

1.2 底层原理(Java 中的 Monitor 实现)

  • 对象与 Monitor 的关联 :Java 中每个对象都天生关联一个 Monitor (即 "对象锁" 的本质),通过对象头(Object Header)中的 Mark Word 存储 Monitor 的关联信息(如锁状态、持有锁的线程 ID)。
  • Monitor 的所属关系
    • 普通对象:创建对象时,JVM 自动为其分配对应的 Monitor(懒加载,首次竞争锁时初始化)。
    • 类对象(Class):静态同步方法的锁本质是类对象的 Monitor。
  • 底层依赖:Java 的 Monitor 基于操作系统的互斥量(Mutex)和条件变量(Condition Variable)实现,但通过 JVM 优化(如偏向锁、轻量级锁)避免直接调用操作系统内核态函数,减少性能开销。

1.3 Monitor 关键组成部分

组成部分 作用
Entry Set(入口队列) 等待获取锁的线程队列,线程竞争锁失败时进入此队列(阻塞状态:BLOCKED)。
Owner(锁持有者) 当前持有 Monitor 锁的线程,同一时间仅能有一个线程成为 Owner。
Wait Set(等待队列) 调用 wait() 后释放锁的线程队列(阻塞状态:WAITING/TIMED_WAITING)。
互斥锁(Mutex) 保证临界区互斥访问的核心组件,控制线程进入临界区的权限。
条件变量(Condition) 支持线程间协作,提供 wait()/notify()/notifyAll() 方法实现等待 - 通知。

二、Monitor 工作流程(线程竞争与协作)

图示:

2.1 锁竞争流程(Entry Set → Owner)

  1. 线程尝试进入临界区(执行 synchronized 代码块 / 方法),首先尝试获取 Monitor 锁。
  2. 若 Monitor 无持有者(锁空闲):当前线程直接成为 Owner,标记 Mark Word 为 "锁定状态",执行临界区代码。
  3. 若 Monitor 已被其他线程持有(锁占用):当前线程进入 Entry Set,状态变为 BLOCKED,等待锁释放。
  4. 当 Owner 线程释放锁(正常退出临界区 / 异常 / 调用 wait()):JVM 从 Entry Set 中唤醒一个线程(非公平,默认策略),该线程重新竞争锁,成功后成为新的 Owner。

2.2 线程协作流程(Owner → Wait Set → Entry Set)

  1. Owner 线程执行过程中,若需等待某个条件满足(如生产者等待队列非空),调用 wait() 方法:
    • 释放当前持有的 Monitor 锁,状态从 RUNNABLE 变为 WAITING(或 TIMED_WAITING,若调用 wait(long))。
    • 线程进入 Wait Set,等待其他线程的通知。
  2. 其他线程(通常是 Owner 线程)完成操作后,调用 notify()notifyAll() 方法:
    • notify():从 Wait Set 中随机唤醒一个线程,该线程移至 Entry Set,状态变为 BLOCKED,等待重新竞争锁。
    • notifyAll():唤醒 Wait Set 中所有线程,所有线程均移至 Entry Set,竞争锁。
  3. 被唤醒的线程竞争锁成功后,重新成为 Owner,从 wait() 方法返回,继续执行后续代码。

2.3 锁释放的三种场景

  1. 线程正常执行完临界区代码,自动释放锁,Owner 变为 null。
  2. 线程在临界区抛出未捕获异常,JVM 自动释放锁。
  3. 线程调用 wait() 方法,主动释放锁,进入 Wait Set。

三、Java 对象头与 Monitor 关联

Java 中 Monitor 与对象紧密绑定,核心是通过对象头存储 Monitor 相关信息,接下来分析对象头结构与 Monitor 的关联逻辑。

1. 对象头的基本结构

Java 对象头由两部分组成(数组对象多一个 array length 字段):

组成部分 作用
Mark Word 存储对象的锁状态、Monitor 指针、HashCode、GC 年龄等核心信息(重点)。
Klass Word 指向对象的类元数据(如 Object.class),确认对象的类型。

2. Mark Word 与 Monitor 的绑定

Mark Word 是对象头的核心,其结构会随锁状态变化,其中 "重量级锁" 状态下会存储 Monitor 的指针(ptr_to_heavyweight_monitor),直接关联对象与 Monitor。

32 位 JVM Mark Word 结构
锁状态 Mark Word 结构(32 位) 核心存储内容
无锁(Normal) hashcode(25) + age(4) + biased_lock(0) + 01 对象哈希码、GC 年龄
偏向锁(Biased) thread(23) + epoch(2) + age(4) + biased_lock(1) + 01 持有偏向锁的线程 ID
轻量级锁(Lightweight) ptr_to_lock_record(30) + 00 线程栈中锁记录的指针
重量级锁(Heavyweight) ptr_to_heavyweight_monitor(30) + 10 Monitor 对象的指针(核心关联)
GC 标记(Marked) 空 + 11 垃圾回收标记
64 位 JVM Mark Word 结构
锁状态 Mark Word 结构(64 位)
无锁(Normal) unused(25) + hashcode(31) + unused(1) + age(4) + biased_lock(0) + 01
重量级锁 ptr_to_heavyweight_monitor(62) + 10

3. 核心结论

  • 当对象被 synchronized 加锁且升级为重量级锁时,Mark Word 中存储 Monitor 的指针,实现对象与 Monitor 的一一对应。
  • 锁升级的本质:Mark Word 结构的变化,从无锁→偏向锁→轻量级锁→重量级锁,Monitor 仅在重量级锁时被完全激活。

四、Monitor 与 synchronized 的深度绑定

4.1 synchronized 依赖 Monitor 实现同步

  • synchronized 的锁本质 :synchronized 是 Monitor 的 "语法糖",其底层完全依赖 Monitor 机制实现:

    • 同步代码块:通过 monitorenter 指令获取 Monitor 锁,monitorexit 指令释放锁(异常时 JVM 自动插入 monitorexit)。
    • 同步方法:通过方法的 ACC_SYNCHRONIZED 标志位,调用方法时自动获取 / 释放 Monitor 锁(锁对象为 this 或类对象)。
  • 代码层面验证

    java 复制代码
    // 同步代码块(锁对象为 lock)
    static final Object lock = new Object();
    synchronized (lock) {
        // 临界区:依赖 Monitor 保证互斥
        counter++;
    }
    
    // 同步方法(锁对象为 this)
    public synchronized void increment() {
        // 临界区:底层是 Monitor 锁
        counter++;
    }

4.2 Monitor 锁的状态升级(JVM 优化)

Java 为提升 Monitor 性能,引入了锁状态升级机制(无锁 → 偏向锁 → 轻量级锁 → 重量级锁),不同状态对应 Monitor 的不同实现方式:

锁状态 适用场景 Monitor 实现方式 性能特点
无锁 无线程竞争 未初始化 Monitor 无需锁开销
偏向锁 单线程重复获取锁 Monitor 记录偏向线程 ID,无需竞争 几乎无锁开销(仅 CAS 操作)
轻量级锁 少量线程交替竞争 线程通过 CAS 操作尝试获取锁,自旋等待 无内核态阻塞开销,适合短临界区
重量级锁 多线程同时竞争 依赖操作系统互斥量(Mutex)实现 Monitor 阻塞 / 唤醒开销大,适合长临界区
  • 核心逻辑:锁状态只能升级不能降级,通过自适应策略(如自旋次数、竞争强度)动态切换,平衡性能与并发安全性。

4.3 线程八锁与 Monitor 锁对象判定(面试重点)

Monitor 的核心是 "锁对象唯一",线程八锁本质是判定 synchronized 对应的 Monitor 锁对象,以下是高频场景:

场景 代码示例 锁对象(Monitor) 执行结果 核心结论
1 同一对象调用两个同步成员方法 this(当前对象的 Monitor) 串行执行(12 或 21) 同一 Monitor 互斥
2 同一对象调用同步方法 + 非同步方法 同步方法锁 this,非同步方法无锁 非同步方法先执行 非同步方法不参与 Monitor 竞争
3 不同对象调用同步静态方法 类对象(Class 的 Monitor) 串行执行 类对象的 Monitor 全局唯一
4 同一对象调用同步成员方法 + 同步静态方法 成员方法锁 this,静态方法锁 Class 并行执行 两个不同的 Monitor,不互斥

五、Monitor 实战案例(基于等待 / 通知机制)

5.1 案例 1:线程安全计数器(Monitor 互斥性)

java 复制代码
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class MonitorCounter {
    // 共享资源
    private int counter = 0;
    // 锁对象(关联 Monitor)
    private final Object lock = new Object();

    // 自增方法(依赖 Monitor 保证原子性)
    public void increment() {
        synchronized (lock) { // monitorenter:获取 Monitor 锁
            counter++;
            log.debug("线程 {} 执行自增,counter = {}", Thread.currentThread().getName(), counter);
        } // monitorexit:释放 Monitor 锁
    }

    public int getCounter() {
        synchronized (lock) {
            return counter;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MonitorCounter counter = new MonitorCounter();
        // 3 个线程并发自增 1000 次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) counter.increment();
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) counter.increment();
        }, "t2");
        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) counter.increment();
        }, "t3");

        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();

        log.debug("最终 counter = {}", counter.getCounter()); // 稳定输出 3000
    }
}
  • 核心原理 :通过 synchronized 绑定 Monitor,保证 counter++ 临界区同一时间仅一个线程执行,避免指令交错。

5.2 案例 2:生产者 - 消费者模型(Monitor 协作性)

java 复制代码
import lombok.extern.slf4j.Slf4j;

import java.util.LinkedList;
import java.util.Queue;

@Slf4j
public class MonitorProducerConsumer {
    // 任务队列(共享资源)
    private final Queue<String> taskQueue = new LinkedList<>();
    // 队列容量
    private static final int CAPACITY = 3;
    // 锁对象(关联 Monitor)
    private final Object lock = new Object();

    // 生产者:添加任务
    public void produce(String task) throws InterruptedException {
        synchronized (lock) {
            // 队列满时,等待消费者消费(进入 Wait Set)
            while (taskQueue.size() >= CAPACITY) {
                log.debug("队列满,生产者 {} 等待", Thread.currentThread().getName());
                lock.wait(); // 释放 Monitor 锁,进入 Wait Set
            }
            // 生产任务
            taskQueue.offer(task);
            log.debug("生产者 {} 生产任务:{},队列大小:{}", Thread.currentThread().getName(), task, taskQueue.size());
            lock.notifyAll(); // 唤醒消费者(从 Wait Set 移至 Entry Set)
        }
    }

    // 消费者:获取任务
    public String consume() throws InterruptedException {
        synchronized (lock) {
            // 队列空时,等待生产者生产(进入 Wait Set)
            while (taskQueue.isEmpty()) {
                log.debug("队列空,消费者 {} 等待", Thread.currentThread().getName());
                lock.wait(); // 释放 Monitor 锁,进入 Wait Set
            }
            // 消费任务
            String task = taskQueue.poll();
            log.debug("消费者 {} 消费任务:{},队列大小:{}", Thread.currentThread().getName(), task, taskQueue.size());
            lock.notifyAll(); // 唤醒生产者(从 Wait Set 移至 Entry Set)
            return task;
        }
    }

    public static void main(String[] args) {
        MonitorProducerConsumer pc = new MonitorProducerConsumer();

        // 2 个生产者线程
        for (int i = 0; i < 2; i++) {
            int producerId = i;
            new Thread(() -> {
                try {
                    for (int j = 0; j < 5; j++) {
                        pc.produce("任务-" + producerId + "-" + j);
                        Thread.sleep(500); // 模拟生产耗时
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "生产者-" + producerId).start();
        }

        // 2 个消费者线程
        for (int i = 0; i < 2; i++) {
            int consumerId = i;
            new Thread(() -> {
                try {
                    for (int j = 0; j < 5; j++) {
                        pc.consume();
                        Thread.sleep(1000); // 模拟消费耗时
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "消费者-" + consumerId).start();
        }
    }
}
  • 核心原理 :利用 Monitor 的 Wait Set 和条件变量,实现生产者与消费者的协作:
    • 队列满时,生产者调用 wait() 释放锁,进入等待;
    • 队列空时,消费者调用 wait() 释放锁,进入等待;
    • 一方完成操作后,调用 notifyAll() 唤醒另一方,避免死等。

六、相关知识点

6.1 Monitor 与 Lock(ReentrantLock)的对比

特性 Monitor(synchronized 底层) Lock(ReentrantLock)
锁获取方式 隐式获取(monitorenter 显式获取(lock()
锁释放方式 隐式释放(monitorexit/ 异常自动释放) 显式释放(unlock(),必须在 finally 中)
可中断性 否(BLOCKED 状态不可中断) 是(lockInterruptibly()
超时获取 是(tryLock(long)
公平锁支持 否(默认非公平,JDK 1.6+ 有自适应公平策略) 是(构造函数指定 true
多条件变量 否(仅一个 Wait Set) 是(newCondition(),多个 Condition 对应多个等待队列)
锁状态查询 无直接 API(需通过线程状态判断) 有(isLocked()/getHoldCount()
适用场景 简单同步场景(代码简洁) 复杂同步场景(如超时、中断、多条件)

6.2 Monitor 常见问题与避坑指南

  1. 虚假唤醒
    • 问题:wait() 可能在未被 notify()/notifyAll() 唤醒的情况下返回(JVM 底层实现导致)。
    • 解决:必须用 while 循环判断条件,而非 if(如案例 2 中 while (taskQueue.isEmpty())),唤醒后重新校验条件。
  2. wait()/notify() 必须在 synchronized 块中
    • 原因:调用这两个方法前,线程必须持有 Monitor 锁(否则抛 IllegalMonitorStateException)。
    • 逻辑:wait() 需要先释放锁,notify() 需要唤醒等待线程竞争锁,均依赖 Monitor 上下文。
  3. notify()notifyAll() 的选择
    • notify():随机唤醒一个线程,可能导致其他线程长期等待(饥饿)。
    • notifyAll():唤醒所有等待线程,避免饥饿,但可能增加锁竞争开销。
    • 建议:多线程协作时优先用 notifyAll(),除非能明确保证唤醒单个线程即可满足条件。
  4. 锁膨胀与性能影响
    • 问题:Monitor 从偏向锁升级为重量级锁后,性能开销大幅增加(内核态阻塞 / 唤醒)。
    • 优化:尽量减小临界区代码长度,避免长时间持有锁;避免多线程同时竞争同一把锁。

6.3 Monitor 的核心特性总结

  1. 互斥性:通过 Owner 机制保证临界区独占访问,解决竞态条件。
  2. 可重入性 :同一线程可多次获取同一 Monitor 锁(Mark Word 记录锁重入次数),避免死锁。
  3. 协作性:通过 Wait Set 和条件变量支持线程间高效通信,无需轮询(降低 CPU 开销)。
  4. 原子性保障:临界区代码在 Monitor 保护下,视为原子操作(避免指令交错)。

6.4 底层字节码分析(Monitor 指令级实现)

以同步代码块为例,查看 monitorentermonitorexit 指令:

java 复制代码
// 原代码
synchronized (lock) {
    counter++;
}

// 编译后的字节码(关键部分)
0: aload_1         // 加载锁对象 lock 到操作数栈
1: monitorenter    // 获取 Monitor 锁(进入临界区)
2: aload_0         // 加载当前对象
3: dup             // 复制对象引用
4: getfield        // 获取 counter 字段
7: iconst_1        // 压入常量 1
8: iadd            // counter + 1
9: putfield        // 赋值给 counter
12: aload_1        // 加载锁对象
13: monitorexit    // 释放 Monitor 锁(正常退出)
14: goto 22        // 跳至方法结束
17: astore_2       // 捕获异常
18: aload_1        // 加载锁对象
19: monitorexit    // 释放 Monitor 锁(异常退出)
20: aload_2        // 抛出异常
21: athrow
22: return
  • 关键:monitorenter 对应锁获取,monitorexit 对应锁释放,异常时 JVM 自动插入 monitorexit,保证锁一定释放。

七、核心 API 详解(Monitor 相关)

API 方法 作用 关键注意点
void wait() 释放 Monitor 锁,进入 Wait Set(无超时等待) 必须在 synchronized 块中;可能虚假唤醒,需配合 while 循环;唤醒后重新竞争锁
void wait(long timeout) 释放 Monitor 锁,进入 TIMED_WAITING 状态(超时自动唤醒) 超时时间单位为毫秒;其他注意点同 wait()
void notify() 从 Wait Set 中随机唤醒一个线程,移至 Entry Set 竞争锁 必须在 synchronized 块中;仅唤醒一个线程,可能导致饥饿
void notifyAll() 唤醒 Wait Set 中所有线程,均移至 Entry Set 竞争锁 必须在 synchronized 块中;避免饥饿,多线程协作优先使用
synchronized 代码块 / 方法 隐式获取 / 释放 Monitor 锁 锁对象为指定对象 /this/ 类对象;自动重入;异常时自动释放锁

八、总结

Monitor 是 Java 并发编程的核心同步机制,是 synchronized 的底层实现基础,核心价值在于通过互斥性保证临界区安全,通过协作性支持线程间高效通信

  • 核心组成:Entry Set(锁竞争队列)、Owner(锁持有者)、Wait Set(等待队列);
  • 核心流程:线程竞争锁(Entry Set → Owner)、线程协作(Owner → Wait Set → Entry Set);
  • 关键拓展:锁状态升级(优化性能)、与 Lock 的对比、虚假唤醒避坑;
  • 适用场景:简单同步用 synchronized(依赖 Monitor),复杂同步用 Lock(灵活拓展)。
相关推荐
布朗克16812 小时前
34 JVM深入理解
java·jvm
啦啦啦啦啦zzzz12 小时前
算法总结(双指针)
c++·算法·双指针
Flittly12 小时前
【AgentScope Java新手村系列】(4)结构化输出
java·spring boot·spring·ai
wzg19690226wzg12 小时前
rust 学习 泛型
开发语言·学习·rust
techdashen12 小时前
Rust 基础设施团队 2025 Q4 回顾与 2026 Q1 计划
开发语言·后端·rust
红宝村村长12 小时前
torch.autograd.Function.apply()
开发语言·python
AI科技星12 小时前
《数术工坊:非欧射影录》类型:硬核光影·几何本源
c语言·开发语言·网络·量子计算·agi
何以解忧,唯有..12 小时前
Python 中的继承机制:从基础到高级用法详解
java·开发语言·python
Yiyaoshujuku12 小时前
化合物数据集API接口(数据结构及样例)
java·网络·数据结构
QiLinkOS12 小时前
极客与商业思维的融合实践(1)
c语言·数据库·c++·人工智能·算法·开源协议