当前位置: 首页 > news >正文

Java 的 Monitor 机制:原理与源码详解

        本文将从底层原理和源代码层面详细解释 Java 的 Monitor 机制,尽量用通俗易懂的语言让初学者也能理解。从概念开始,逐步深入到实现细节,涵盖 Monitor 的作用、结构、源码分析,并提供完整的步骤和推导。由于 Monitor 是 Java 锁机制(尤其是 synchronized)的核心,我会结合对象头和锁的场景增强理解。


一、什么是 Java 的 Monitor?为什么需要它?

1.1 Monitor 的通俗概念

        Monitor(监视器)是 Java 中实现线程同步的核心机制,可以看作一个“独占门锁”。想象一个只有一张票的电影院(共享资源),Monitor 就像检票员,确保同一时间只有一个观众(线程)能进去看电影,其他人得在门口排队等着。

        在 Java 中,Monitor 是 synchronized 关键字的底层实现,用于保证多线程访问共享资源时的线程安全。每个 Java 对象都可以关联一个 Monitor,充当锁的角色。

1.2 为什么需要 Monitor?

        在多线程编程中,多个线程可能同时访问共享资源(比如一个变量或对象),如果没有协调机制,会导致数据不一致。例如:

public class Counter {private int count = 0;public void increment() {count++; // 看似简单,实际包含读、加、写三步}
}

count++ 包含三个步骤:

  1. 读取 count 的值。
  2. 加 1。
  3. 写回新值。

如果两个线程同时执行 increment(),可能出现:

  • 线程 A 读取 count = 0
  • 线程 B 读取 count = 0
  • 线程 A 计算 0 + 1 = 1,写回 count = 1
  • 线程 B 计算 0 + 1 = 1,写回 count = 1

结果是两次加操作后,count 仍为 1,而不是 2。Monitor 通过确保同一时间只有一个线程执行关键代码(临界区),解决了这个问题。

通俗解释

  • Monitor 像一个“排队系统”,确保只有一个线程能“进门”操作共享资源,其他线程在外面等着。

二、Monitor 在 Java 中的作用

Monitor 是 synchronized 关键字的底层实现,负责:

  1. 互斥访问:同一时间只有一个线程能持有 Monitor,进入临界区。
  2. 线程协调:支持线程的等待和唤醒(通过 wait()、notify()、notifyAll())。
  3. 锁状态管理:记录锁的持有者、等待队列等。

Monitor 与对象头紧密相关,对象头的 Mark Word 在重量级锁状态下会指向 Monitor 实例。


三、Monitor 的工作原理

3.1 Monitor 的核心组件

Monitor 是一个操作系统级别的同步工具,通常包含以下部分:

  1. Owner(持有者):当前持有锁的线程。
  2. Entry Set(进入队列):等待获取锁的线程队列。
  3. Wait Set(等待队列):调用 wait() 后释放锁并等待的线程。
  4. Recursions(重入计数):记录同一线程重入锁的次数(支持可重入锁)。

通俗例子

  • 想象一个厕所(共享资源),只有一个坑位,Monitor 是门上的智能锁:
    • Owner:正在用厕所的人(当前线程)。
    • Entry Set:在门口排队等坑位的人(等待锁的线程)。
    • Wait Set:暂时出去喝咖啡、等着被叫回来的人(调用 wait() 的线程)。
    • Recursions:记录同一个人反复进出厕所的次数(重入锁)。

3.2 Monitor 的状态转换

Monitor 的工作涉及以下状态和操作:

  1. 获取锁(Enter)
    • 线程尝试进入 Monitor。
    • 如果 Monitor 空闲,线程成为 Owner。
    • 如果已被占用,线程进入 Entry Set 等待。
  2. 执行临界区:Owner 线程执行 synchronized 代码。
  3. 释放锁(Exit)
    • Owner 线程退出临界区,释放 Monitor。
    • 唤醒 Entry Set 中的一个线程(或 Wait Set 中的线程,如果有 notify())。
  4. 等待和唤醒
    • 线程调用 wait(),释放 Monitor,进入 Wait Set。
    • 其他线程调用 notify() 或 notifyAll(),唤醒 Wait Set 中的线程。

四、Monitor 的底层实现

        Monitor 的实现主要在 HotSpot JVM 的 C++ 源码中,核心类是 ObjectMonitor,位于 src/hotspot/share/runtime/objectMonitor.hpp。以下从源码和原理逐步分析。

4.1 ObjectMonitor 的结构

ObjectMonitor 是 HotSpot JVM 中 Monitor 的具体实现,包含以下关键字段(简化版):

class ObjectMonitor {
private:volatile Thread* _owner;         // 当前持有锁的线程volatile intptr_t _recursions;   // 重入次数ObjectWaiter* _EntryList;       // 等待锁的线程队列(Entry Set)ObjectWaiter* _WaitSet;         // 等待notify的线程队列(Wait Set)markOop _header;                // 保存对象的Mark Wordvolatile int _count;             // 等待线程数volatile int _waiters;           // Wait Set中的线程数// ...
};

字段解释

  1. _owner:指向当前持有 Monitor 的线程。如果为空,表示 Monitor 空闲。
  2. _recursions:记录重入次数。例如,线程多次进入同一 synchronized 块,计数加 1。
  3. _EntryList:一个链表,存储等待获取锁的线程(阻塞状态)。
  4. _WaitSet:一个链表,存储调用 wait() 的线程(等待被唤醒)。
  5. _header:保存对象头的 Mark Word,锁释放时恢复。
  6. _count:Entry Set 中的线程数,用于优化。
  7. _waiters:Wait Set 中的线程数,用于管理等待线程。

通俗解释

  • ObjectMonitor 像一个智能门锁的控制面板,记录“谁在用”(Owner)、“谁在排队”(EntryList)、“谁在休息”(WaitSet)。

4.2 Monitor 的工作流程

以下是 Monitor 的核心操作流程,结合源码分析:

4.2.1 获取锁(enter)

当线程执行 synchronized 代码时,JVM 调用 ObjectMonitor::enter:

void ObjectMonitor::enter(Thread* self) {if (_owner == self) {_recursions++; // 重入,增加计数return;}if (_owner == nullptr && Atomic::cmpxchg(self, &_owner, nullptr) == nullptr) {// 无人持有,CAS设置自己为Ownerreturn;}// 锁被占用,进入EntryListadd_to_entry_list(self);self->park(); // 线程阻塞
}

步骤

  1. 检查重入
    • 如果当前线程已经是 _owner,说明是重入锁,增加 _recursions 计数,直接返回。
    • 通俗解释:如果厕所里的人是你自己,就不用再排队,直接继续用。
  2. 尝试获取空闲锁
    • 如果 _owner 为空,用 CAS(Compare-And-Swap)原子操作设置 _owner 为当前线程。
    • CAS 确保多线程竞争时只有一个线程成功。
    • 通俗解释:如果厕所没人,赶紧把门锁上,标上“我的名字”。
  3. 进入等待队列
    • 如果锁被占用,线程加入 _EntryList,调用 park() 阻塞自己。
    • 通俗解释:如果有人在用,就去门口排队,暂时“睡着”。
4.2.2 释放锁(exit)

当线程退出 synchronized 块时,JVM 调用 ObjectMonitor::exit:

void ObjectMonitor::exit(Thread* self) {if (_recursions > 0) {_recursions--; // 减少重入计数return;}if (_owner != self) {return; // 非Owner线程不能释放}_owner = nullptr; // 释放锁if (_EntryList != nullptr || _WaitSet != nullptr) {notify_waiters(); // 唤醒等待线程}
}

步骤

  1. 检查重入
    • 如果 _recursions 大于 0,减少计数,不释放锁。
    • 通俗解释:你还没完全“离开”厕所,只是少用了一次。
  2. 验证 Owner
    • 确保当前线程是 _owner,防止非法释放。
    • 通俗解释:只有用厕所的人才能开锁。
  3. 释放锁
    • 设置 _owner 为 nullptr,表示锁空闲。
    • 通俗解释:把门锁打开,标上“没人用”。
  4. 唤醒等待线程
    • 如果 _EntryList 或 _WaitSet 不为空,唤醒一个或多个线程(通过 unpark())。
    • 通俗解释:喊一声“下一个”,让排队的人进来。
4.2.3 等待(wait)

当线程调用 Object.wait() 时,JVM 调用 ObjectMonitor::wait:

void ObjectMonitor::wait(Thread* self, jlong millis) {if (_owner != self) {return; // 非Owner不能wait}// 保存状态ObjectWaiter node(self);_WaitSet->append(&node); // 加入Wait Set_recursions = 0; // 重置重入计数exit(self); // 释放锁self->park(millis); // 阻塞等待
}

步骤

  1. 验证 Owner
    • 确保当前线程是 _owner,因为只有锁持有者能调用 wait()。
    • 通俗解释:只有在厕所里的人才能说“我先出去等会儿”。
  2. 加入 Wait Set
    • 创建一个 ObjectWaiter 节点,加入 _WaitSet。
    • 通俗解释:把名字写在“休息区”名单上。
  3. 释放锁
    • 重置 _recursions,调用 exit() 释放锁。
    • 通俗解释:开门出去,让别人用厕所。
  4. 阻塞线程
    • 调用 park(millis),线程阻塞(如果指定了超时时间 millis)。
    • 通俗解释:去旁边“睡着”,等着被叫醒。
4.2.4 唤醒(notify/notifyAll)

当线程调用 Object.notify() 或 notifyAll() 时,JVM 调用 ObjectMonitor::notify:

void ObjectMonitor::notify(TRAPS) {if (_WaitSet == nullptr) {return; // 没有等待线程}ObjectWaiter* waiter = _WaitSet->remove_first(); // 取出一个线程add_to_entry_list(waiter->thread()); // 加入EntryListwaiter->thread()->unpark(); // 唤醒线程
}void ObjectMonitor::notifyAll(TRAPS) {while (_WaitSet != nullptr) {notify(); // 逐个唤醒}
}

步骤

  1. 检查 Wait Set
    • 如果 _WaitSet 为空,直接返回。
    • 通俗解释:如果休息区没人,不用喊。
  2. 唤醒线程(notify)
    • 从 _WaitSet 取出一个线程,加入 _EntryList,调用 unpark() 唤醒。
    • 通俗解释:叫醒休息区的一个人,让他去排队抢锁。
  3. 唤醒所有线程(notifyAll)
    • 循环调用 notify(),唤醒 _WaitSet 中所有线程。
    • 通俗解释:喊“大家都回来排队”,让休息区所有人去抢锁。

4.3 Monitor 与对象头的交互

Monitor 与对象头的 Mark Word 紧密相关:

  • 当锁升级为重量级锁,Mark Word 存储指向 ObjectMonitor 的指针(锁标志位为 10)。
  • ObjectMonitor 的 _header 字段保存原始 Mark Word(包括哈希码、GC 年龄等)。
  • 释放锁时,JVM 将 _header 恢复到对象头的 Mark Word。

通俗解释

  • Mark Word 像门上的“地址牌”,重量级锁时指向“保安室”(Monitor)。
  • Monitor 像保安室,记录门的原始信息(_header),解锁时把地址牌换回去。

4.4 字节码层面的支持

synchronized 代码被编译为字节码指令 monitorenter 和 monitorexit:

synchronized(obj) {count++;
}

字节码(简化)

monitorenter // 获取Monitor
iload count
iadd 1
istore count
monitorexit // 释放Monitor
  • monitorenter:调用 ObjectMonitor::enter,尝试获取锁。
  • monitorexit:调用 ObjectMonitor::exit,释放锁。

五、Monitor 的底层操作系统支持

Monitor 的阻塞和唤醒依赖操作系统的线程调度机制:

  1. park/unpark
    • park():阻塞线程,底层调用操作系统的 pthread_cond_wait(Linux)或类似机制。
    • unpark():唤醒线程,底层调用 pthread_cond_signal 或 futex(Linux)。
  2. 互斥锁(Mutex)
    • Monitor 的互斥性通过操作系统的 pthread_mutex(Linux)或类似机制实现。
    • CAS 操作(Atomic::cmpxchg)依赖 CPU 的原子指令(如 cmpxchg)。

通俗解释

  • Monitor 像一个“智能门锁”,但真正的“锁芯”是操作系统提供的。
  • 线程“睡着”或“醒来”靠操作系统调度,就像保安喊“下一个”。

六、Monitor 的优化

JVM 对 Monitor 进行了大量优化,减少性能开销:

  1. 偏向锁
    • 如果锁通常被同一线程持有,Mark Word 记录线程 ID,避免创建 Monitor。
    • 通俗解释:给常来的人发“VIP卡”,不用每次找保安。
  2. 轻量级锁
    • 低竞争时,使用 CAS 操作锁记录,不创建 Monitor。
    • 通俗解释:用“临时钥匙”代替保安,速度更快。
  3. 重量级锁
    • 高竞争时才使用 Monitor,依赖操作系统。
    • 通俗解释:人太多,只能请保安(Monitor)来管秩序。
  4. 自旋锁
    • 线程在 park() 前可能短暂自旋(循环尝试),避免立即阻塞。
    • 通俗解释:排队时先看一眼门开了没,省得直接睡着。

这些优化通过对象头的 Mark Word 动态切换锁状态(无锁 → 偏向锁 → 轻量级锁 → 重量级锁)。


七、完整推导流程

  1. 问题背景

    • 多线程并发访问共享资源可能导致数据不一致,需通过锁保证线程安全。
    • Monitor 是 synchronized 的核心实现,管理互斥和线程协调。
  2. Monitor 的结构

    • 包含 Owner、EntryList、WaitSet、Recursions 等,记录锁状态和线程队列。
    • 与对象头的 Mark Word 交互,重量级锁时存储 Monitor 指针。
  3. 工作流程

    • 获取锁:检查重入、尝试 CAS 获取、或加入 EntryList 阻塞。
    • 释放锁:减少重入计数、置空 Owner、唤醒等待线程。
    • 等待/唤醒:通过 Wait Set 实现 wait() 和 notify(),涉及锁释放和重新获取。
  4. 底层实现

    • ObjectMonitor 类管理 Monitor 逻辑,源码在 objectMonitor.cpp。
    • 依赖操作系统 Mutex 和线程调度(park/unpark)。
  5. 优化

    • 偏向锁、轻量级锁减少 Monitor 使用,重量级锁作为 fallback。
    • 自旋锁和 CAS 提高效率。

八、通俗总结

  • Monitor 是什么?它是 synchronized 的“门锁”,确保线程排队访问共享资源。
  • 怎么工作?像一个智能保安,记录“谁在用”(Owner)、“谁在等”(EntryList/WaitSet),支持等待和唤醒。
  • 为什么重要?没有 Monitor,synchronized 无法实现线程安全和协调。
  • 底层实现?通过 HotSpot JVM 的 ObjectMonitor 类,依赖操作系统互斥锁和调度。

生活化比喻

  • Monitor 像一个厕所的智能门锁:
    • 有人用时,标上“占用”(Owner)。
    • 有人排队时,记下名单(EntryList)。
    • 有人出去喝咖啡时,记在休息区(WaitSet)。
    • 支持“熟客”反复进出(重入),还能喊人回来(notify)。

九、源码分析补充

以下是 ObjectMonitor 的关键方法(伪代码简化版),进一步说明逻辑:

// 获取锁
void ObjectMonitor::enter(Thread* self) {if (_owner == self) {_recursions++;return;}if (_owner == nullptr && Atomic::cmpxchg(self, &_owner, nullptr) == nullptr) {return;}add_to_entry_list(self);self->park();
}// 释放锁
void ObjectMonitor::exit(Thread* self) {if (_recursions > 0) {_recursions--;return;}_owner = nullptr;if (_EntryList || _WaitSet) {notify_waiters();}
}// 等待
void ObjectMonitor::wait(Thread* self, jlong millis) {_WaitSet->append(self);_recursions = 0;exit(self);self->park(millis);
}// 唤醒
void ObjectMonitor::notify() {if (_WaitSet) {Thread* t = _WaitSet->remove_first();_EntryList->append(t);t->unpark();}
}

关键点

  • CAS:确保 _owner 设置的原子性。
  • park/unpark:依赖 LockSupport,底层调用操作系统调度。
  • 队列管理ObjectWaiter 节点维护 EntryList 和 WaitSet。

十、扩展阅读

  1. 源码推荐
    • HotSpot JVM:objectMonitor.hpp 和 objectMonitor.cpp(Monitor 实现)。
    • 相关文件:synchronizer.cpp(锁状态切换)、markOop.hpp(Mark Word)。
  2. 工具
    • 用 jstack 查看线程状态,分析 Monitor 竞争。
    • 用 JOL(Java Object Layout)查看对象头和 Monitor 指针。
  3. 书籍
    • 《深入理解 Java 虚拟机》(周志明):讲解 JVM 锁和 Monitor 实现。
    • 《Java 并发编程实战》:结合 synchronized 理解 Monitor。

相关文章:

  • [git]如何关联本地分支和远程分支
  • 数据库的进阶操作
  • 路由器断流排查终极指南:从Ping测试到Wireshark抓包5步定位法
  • 基于GlusterFS的分布式存储集群部署实战指
  • System-V 共享内存
  • JavaScript中数组和对象不同遍历方法的顺序规则
  • 驱动开发硬核特训 · Day 30(上篇):深入理解 I2C 总线驱动模型(以 at24 EEPROM 为例)
  • 多模态文档检索开源方案-三大竞赛获奖方案技术链路
  • 基于Credit的流量控制
  • C++ 算法学习之旅:从入门到精通的秘籍
  • C++模板笔记
  • Linux系统编程---进程间Signal信号通信
  • el-table计算表头列宽,不换行显示
  • SKNet、空间注意力介绍
  • 如何使用Java生成图像验证码
  • Python学习笔记--Django的安装和简单使用(一)
  • 基于php人力劳务招聘系统开发功能需求分析【源码】
  • window.open(url) 和 window.location.href = url
  • 【PostgreSQL】超简单的主从节点部署
  • python+open3d获取点云的最小外接球体及使用球体裁剪点云
  • 国家主席习近平同普京总统出席签字和合作文本交换仪式
  • 欧派家居:一季度营收降4.8%,目前海外业务整体体量仍较小
  • 黄晨光任中科院空间应用工程与技术中心党委书记、副主任
  • 圆桌丨中俄权威专家详解:两国携手维护战后国际秩序,捍卫国际公平正义
  • 江淮、极氪、奇瑞,排着队造“劳斯莱斯”
  • 中国经济新动能|警惕数字时代下经济的“四大极化”效应