Java中的对象监视器(Monitor)与同步原语synchronized的底层实现详解
字数 2824 2025-12-10 20:40:06

Java中的对象监视器(Monitor)与同步原语synchronized的底层实现详解

1. 知识描述
在Java并发编程中,synchronized关键字是实现线程同步、保证数据一致性的核心机制。其背后的核心概念是对象监视器(Monitor),这是一种经典的线程同步工具。简单来说,每个Java对象在JVM层面都与一个监视器关联,synchronized正是通过获取和释放这个监视器来实现对临界区(同步代码块/方法)的互斥访问。理解对象监视器的工作机制,是深入理解Java同步原理的关键。

2. 循序渐进的解题/讲解过程

步骤一:对象头与Monitor的基础关联

  1. 对象内存布局:在HotSpot JVM中,一个对象在堆内存中的存储布局分为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
  2. 对象头是关键:对象头是承载对象监视器信息的主要结构。它主要包含两部分:
    • Mark Word:用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志等。它也是实现锁(监视器)的关键
    • Klass Pointer:指向对象元数据(即它的类)的指针。
  3. 锁状态:Mark Word中的标志位(Lock Bits)指明了对象的锁状态。在64位JVM中,Mark Word通常为64位,其存储内容会根据锁状态变化而变化。主要状态包括:无锁(01)、偏向锁(01)、轻量级锁(00)、重量级锁(10),以及GC标记(11)等。

步骤二:对象如何关联到真正的Monitor

  1. 重量级锁的Monitor:当我们提到经典的“对象监视器”(Monitor)时,通常特指重量级锁状态下的那个同步机制。它是一个复杂的、由操作系统提供的互斥量(mutex)和条件变量等构建的同步结构。
  2. 关联过程:当线程尝试获取一个对象的锁(进入synchronized块),并且竞争升级到重量级锁时,JVM会在堆中创建一个ObjectMonitor对象(C++实现)。这个ObjectMonitor是Monitor机制的具体实现。
  3. 指针存储:此时,对象的Mark Word中不再存储原始的哈希码或年龄等信息,而是被替换为一个指向这个ObjectMonitor对象的指针。这个操作是通过CAS(比较并交换)原子操作完成的。从此,这个Java对象就与一个重量级的Monitor绑定在了一起。

步骤三:经典的ObjectMonitor结构及其工作原理
重量级锁的ObjectMonitor(在HotSpot源码中为ObjectMonitor.hpp)核心结构和工作队列如下:

  • _owner:指向持有该监视器(锁)的线程。初始为NULL
  • _WaitSet:等待集合。调用wait()方法的线程会被放入此队列。这些线程在等待某个条件(由notify()/notifyAll()触发)。
  • _EntryList:入口队列。竞争锁失败的线程(在synchronized入口处阻塞)会被放入此队列。
  • _recursions:锁的重入次数。因为synchronized是可重入的。

其工作流程(简化)如下:

  1. 线程尝试进入(ENTER):当线程T1希望进入synchronized(obj)保护的代码块时,它首先会通过原子操作尝试将obj关联的ObjectMonitor_owner字段设置为指向自己。
  2. 成功获取:如果_ownerNULL,则设置成功,T1获得锁,进入同步代码块执行,并将_recursions设为1。
  3. 重入:如果_owner已经是T1自己,则_recursions加1,这是可重入性的体现。
  4. 竞争失败:如果_owner是其他线程T2,则T1的获取尝试失败。
  5. 进入EntryList:T1会通过自旋(早期)或直接进入_EntryList队列进行阻塞等待。在重量级锁下,这个阻塞是由操作系统内核的互斥量(mutex)实现的,涉及到线程从用户态到内核态的切换,成本较高。
  6. 释放锁(EXIT):当持有锁的线程T2退出同步代码块时,它会将_recursions减1。如果_recursions变为0,则将_owner设置为NULL
  7. 唤醒竞争线程:T2会从_EntryList_WaitSet中(根据不同的唤醒策略)唤醒一个或多个等待线程。被唤醒的线程会重新尝试竞争锁(设置_owner)。

步骤四:从synchronized关键字到Monitor的完整路径
一个synchronized方法的执行,在字节码层面和JVM执行层面是这样的:

  1. 字节码指令:对于同步代码块,编译后会生成monitorentermonitorexit指令。对于同步方法,方法的访问标志ACC_SYNCHRONIZED会被设置。
  2. 解释执行:当解释器执行到monitorenter指令或进入ACC_SYNCHRONIZED方法时,会调用JVM的运行时函数(如InterpreterRuntime::monitorenter)。
  3. 锁升级过程:JVM并不会立即使用重量级的ObjectMonitor。为了优化性能,它采用了锁升级策略:
    • 偏向锁:假设只有一个线程使用锁。它会在Mark Word中记录线程ID,后续该线程进入就像无锁一样快捷。
    • 轻量级锁:当有轻微竞争时,线程会在自己的栈帧中创建锁记录(Lock Record),并尝试通过CAS将Mark Word复制到锁记录,并替换为指向锁记录的指针。成功则获得锁。这发生在用户态,通过自旋避免内核切换。
    • 重量级锁:当轻量级锁自旋失败(竞争加剧),锁会膨胀。此时,JVM才会创建或关联ObjectMonitor,并将线程推入_EntryList进行真正的阻塞。这就是我们上面详细描述的经典Monitor机制。
  4. 最终关联:在重量级锁状态下,synchronized的互斥语义就是由这个ObjectMonitor通过操作系统的互斥原语来保障的。

总结
synchronized的同步能力,其底层基石是与每个对象关联的监视器(Monitor)。在无竞争或低竞争时,JVM通过偏向锁、轻量级锁等优化手段避免Monitor的昂贵开销。而在高竞争场景下,锁会膨胀为重量级锁,此时对象通过Mark Word指向一个ObjectMonitor结构,该结构通过_owner_EntryList_WaitSet等队列,在操作系统内核的支持下,实现了线程间的互斥与条件等待。理解这个过程,就掌握了synchronized从语法关键字到底层系统调用的完整脉络。

Java中的对象监视器(Monitor)与同步原语synchronized的底层实现详解 1. 知识描述 在Java并发编程中, synchronized 关键字是实现线程同步、保证数据一致性的核心机制。其背后的核心概念是 对象监视器(Monitor) ,这是一种经典的线程同步工具。简单来说,每个Java对象在JVM层面都与一个监视器关联, synchronized 正是通过获取和释放这个监视器来实现对临界区(同步代码块/方法)的互斥访问。理解对象监视器的工作机制,是深入理解Java同步原理的关键。 2. 循序渐进的解题/讲解过程 步骤一:对象头与Monitor的基础关联 对象内存布局 :在HotSpot JVM中,一个对象在堆内存中的存储布局分为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。 对象头是关键 :对象头是承载对象监视器信息的主要结构。它主要包含两部分: Mark Word :用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志等。 它也是实现锁(监视器)的关键 。 Klass Pointer :指向对象元数据(即它的类)的指针。 锁状态 :Mark Word中的标志位(Lock Bits)指明了对象的锁状态。在64位JVM中,Mark Word通常为64位,其存储内容会根据锁状态变化而变化。主要状态包括: 无锁(01)、偏向锁(01)、轻量级锁(00)、重量级锁(10) ,以及GC标记(11)等。 步骤二:对象如何关联到真正的Monitor 重量级锁的Monitor :当我们提到经典的“对象监视器”(Monitor)时,通常特指 重量级锁 状态下的那个同步机制。它是一个复杂的、由操作系统提供的互斥量(mutex)和条件变量等构建的同步结构。 关联过程 :当线程尝试获取一个对象的锁(进入 synchronized 块),并且竞争升级到重量级锁时,JVM会在堆中创建一个 ObjectMonitor 对象(C++实现)。这个 ObjectMonitor 是Monitor机制的具体实现。 指针存储 :此时,对象的Mark Word中不再存储原始的哈希码或年龄等信息,而是被替换为一个 指向这个 ObjectMonitor 对象的指针 。这个操作是通过CAS(比较并交换)原子操作完成的。从此,这个Java对象就与一个重量级的Monitor绑定在了一起。 步骤三:经典的ObjectMonitor结构及其工作原理 重量级锁的 ObjectMonitor (在HotSpot源码中为 ObjectMonitor.hpp )核心结构和工作队列如下: _owner :指向持有该监视器(锁)的线程。初始为 NULL 。 _WaitSet :等待集合。调用 wait() 方法的线程会被放入此队列。这些线程在等待某个条件(由 notify()/notifyAll() 触发)。 _EntryList :入口队列。竞争锁失败的线程(在 synchronized 入口处阻塞)会被放入此队列。 _recursions :锁的重入次数。因为 synchronized 是可重入的。 其工作流程(简化)如下: 线程尝试进入(ENTER) :当线程T1希望进入 synchronized(obj) 保护的代码块时,它首先会通过原子操作尝试将 obj 关联的 ObjectMonitor 的 _owner 字段设置为指向自己。 成功获取 :如果 _owner 为 NULL ,则设置成功,T1获得锁,进入同步代码块执行,并将 _recursions 设为1。 重入 :如果 _owner 已经是T1自己,则 _recursions 加1,这是可重入性的体现。 竞争失败 :如果 _owner 是其他线程T2,则T1的获取尝试失败。 进入EntryList :T1会通过自旋(早期)或直接进入 _EntryList 队列进行阻塞等待。在重量级锁下,这个阻塞是由操作系统内核的互斥量(mutex)实现的,涉及到线程从用户态到内核态的切换,成本较高。 释放锁(EXIT) :当持有锁的线程T2退出同步代码块时,它会将 _recursions 减1。如果 _recursions 变为0,则将 _owner 设置为 NULL 。 唤醒竞争线程 :T2会从 _EntryList 或 _WaitSet 中(根据不同的唤醒策略)唤醒一个或多个等待线程。被唤醒的线程会重新尝试竞争锁(设置 _owner )。 步骤四:从synchronized关键字到Monitor的完整路径 一个 synchronized 方法的执行,在字节码层面和JVM执行层面是这样的: 字节码指令 :对于同步代码块,编译后会生成 monitorenter 和 monitorexit 指令。对于同步方法,方法的访问标志 ACC_SYNCHRONIZED 会被设置。 解释执行 :当解释器执行到 monitorenter 指令或进入 ACC_SYNCHRONIZED 方法时,会调用JVM的运行时函数(如 InterpreterRuntime::monitorenter )。 锁升级过程 :JVM并不会立即使用重量级的 ObjectMonitor 。为了优化性能,它采用了 锁升级 策略: 偏向锁 :假设只有一个线程使用锁。它会在Mark Word中记录线程ID,后续该线程进入就像无锁一样快捷。 轻量级锁 :当有轻微竞争时,线程会在自己的栈帧中创建锁记录(Lock Record),并尝试通过CAS将Mark Word复制到锁记录,并替换为指向锁记录的指针。成功则获得锁。这发生在用户态,通过自旋避免内核切换。 重量级锁 :当轻量级锁自旋失败(竞争加剧),锁会膨胀。此时,JVM才会创建或关联 ObjectMonitor ,并将线程推入 _EntryList 进行真正的阻塞。这就是我们上面详细描述的经典Monitor机制。 最终关联 :在重量级锁状态下, synchronized 的互斥语义就是由这个 ObjectMonitor 通过操作系统的互斥原语来保障的。 总结 : synchronized 的同步能力,其底层基石是 与每个对象关联的监视器(Monitor) 。在无竞争或低竞争时,JVM通过偏向锁、轻量级锁等优化手段避免Monitor的昂贵开销。而在高竞争场景下,锁会膨胀为重量级锁,此时对象通过Mark Word指向一个 ObjectMonitor 结构,该结构通过 _owner 、 _EntryList 、 _WaitSet 等队列,在操作系统内核的支持下,实现了线程间的互斥与条件等待。理解这个过程,就掌握了 synchronized 从语法关键字到底层系统调用的完整脉络。