并发编程原理与实战(二十七)深入剖析synchronized底层基石ObjectMonitor与对象头Mark Word
上一篇文章中,我们深入分析了synchronized底层原理实现,其中分析了什么是对象的监视器,如何查看对象监视器的状态信息、显式隐式获取监视器锁等问题。本文继续深入分析synchronized底层原理实现,解决以下几个问题。
(1)synchronized以什么作为互斥?
(2)synchronized既然可重入,那么重入计数记录在哪里?
(3)持有监视器锁的线程信息记录在哪里?
(4)其他抢锁的等待者记录在哪里?
(5)java对象(锁对象)如何关联Monitor对象?
深入Monitor
Monitor实现并发协同实现原理
synchronized既然是一个独占锁,就像数据库的主键字段不能重复,同一个文件夹下文件名不能重复,那么它的互斥属性是怎么实现的?
synchronized基于Monitor实现,而Monitor是一种编程语言层面的高级的同步机制,用于在多线程环境下管理共享资源的访问,其核心机制包括互斥与同步两部分。Monitor的互斥属性通过内置的互斥锁(Mutex)实现,保证同一时刻仅有一个线程能进入Monitor内部(临界区)执行操作,其他线程需在等待队列中等待。前面我们已经知道,synchronized与wait/notify实现的并发协同是基于条件判断实现的,这个正是基于Monitor的条件变量实现线程间的协作,Monitor内部维护多个队列(如入口队列、条件队列)管理线程状态,确保并发协同的正确性。
互斥锁(Mutex,全称 Mutual Exclusion)是操作系统层面用于控制多线程对共享资源访问的同步机制,其核心目标是确保同一时刻仅有一个线程能进入临界区。
综上所述,我们得出这个依赖关系:synchronized—>Monitor—>Mutex。
JVM ObjectMonitor结构体
synchronized是JVM层面上的锁,而JVM是c/c++写的,在JDK的源码中有对象监视器的结构体ObjectMonitor的定义,ObjectMonitor是HotSpot虚拟机中实现Java对象监视器的核心数据结构。在JDK的源码路径下:
jdk-master\jdk-master\src\hotspot\share\runtime\objectMonitor.cpp
jdk-master\jdk-master\src\hotspot\share\runtime\objectMonitor.hpp
我们摘取其中的构造函数进行分析:
ObjectMonitor::ObjectMonitor() {_header = NULL; // 存储关联对象的Mark Word初始值_count = 0; // 记录锁的重入次数_waiters = 0; // 等待线程计数器(如调用wait()的线程)_recursions = 0; // 锁的重入深度_object = NULL; // 关联的Java对象指针_owner = NULL; // 持有锁的线程指针_cxq = NULL; // 竞争锁失败的线程栈(FILO结构)_EntryList = NULL; // 阻塞线程的双向链表(锁分配缓冲区)_WaitSet = NULL; // 调用wait()的线程等待队列_WaitSetLock = 0; // 保护_WaitSet操作的锁_SpinFreq = 0; // 自旋锁优化参数_SpinClock = 0; // 自旋锁时间控制
}
如上所示,其构造函数初始化了以下关键字段:
1、基本状态字段
(1)header:存储锁对象的Mark Word原始值,初始为NULL。
(2)count:记录线程获取锁的次数,初始为0。
(3)waiters:等待线程计数器,初始为0。
(4)_recursions:锁重入次数,初始为0。
2、线程管理队列
(1)cxq:竞争锁失败的线程单向链表(FILO栈结构),初始为NULL。
(2)EntryList:阻塞线程的双向循环链表,作为锁分配的缓冲区,初始为NULL。
(3)_WaitSet:调用wait()的线程等待队列,初始为NULL。
3、所有权标识
(1)owner:指向持有锁的线程,初始为NULL。
(2)object:关联的Java对象指针,初始为NULL。
4、辅助控制字段
(1)WaitSetLock:保护WaitSet操作的锁,初始为0。
(2)SpinFreq和SpinClock:*自旋锁优化相关参数,初始为0。
该构造函数通过清零所有字段完成初始化,为后续锁竞争(如monitorenter指令)和线程同步(如wait/notify)提供基础支持。其设计体现了Monitor的互斥与同步机制,通过队列管理实现线程阻塞和唤醒。
对象头
结构体ObjectMonitor各个字段的定义解答了文章开头的第1、第2、第3、第4个问题,还剩“java对象(锁对象)如何关联Monitor对象?”这个问题没有解决。
我们注意到,结构体ObjectMonitor中的_header字段存储关联对象的Mark Word初始值。那么什么是Mark Word?要了解Mark Word得先了解java的对象头。
Java对象头是JVM实现对象内存管理、锁机制和GC的关键数据结构,其组成与锁状态动态关联。Java对象核心结构由以下几部分组成。
组成部分 | 大小(64位JVM) | 功能描述 |
---|---|---|
Mark Word | 8字节 | 动态存储对象的运行时数据,包括哈希码(HashCode)、GC分代年龄、锁状态标志、偏向线程ID、锁指针等 |
Klass Pointer | 4/8字节 | 指向方法区中对象的类元数据,用于确定对象所属类型 |
Array Length | 4字节(可选) | 仅数组对象存在,记录数组长度 |
Mark Word与锁优化
Mark Word部分存储着锁状态信息,而锁状态有无锁、偏向锁、轻量级锁、重量级锁这几种状态。所以对象头的Mark Word是synchronized锁机制的关键实现。锁状态的变化遵循不可逆的过程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
(1)无锁状态。对象刚创建且未被任何线程加锁时处于无锁状态。锁标志位为01。
(2)偏向锁。首次有线程访问同步块时,升级为偏向锁;第一个线程访问同步块,且无竞争;消除单线程重复加锁的开销。锁标志位变为101。
(3)轻量级锁。第二个线程尝试获取锁(发生竞争),偏向锁撤销并升级为轻量级锁。锁标志位变为00。
(4)重量级锁。当CAS自旋失败(默认10次)或竞争加剧(如第三个线程加入),升级为重量级锁。对象头指向操作系统级互斥量(monitor),锁标志位变为10。
锁标志位状态转换示意流程:
无锁 (01) │↓ (首次单个线程访问)
偏向锁 (101) │↓ (多个线程访问,竞争发生)
轻量级锁 (00) │↓ (多个线程访问,CAS自旋失败或竞争加剧)
重量级锁 (10)
在《并发编程原理与实战(二十三)StampedLock应用实战与各种锁性能对比分析》这篇文章中,我们对比了包括synchronized在内的四种锁的性能,其中synchronized的表现最好。从上述锁标志位状态转换过程可以看出,根据竞争激烈程度自动调整锁策略,这其实是一个synchronized锁优化的结果。
线程执行synchronized(obj)时,JVM会检查obj对象的Mark Word锁标志位。当锁需要升级为重量级锁时,则创建或关联已有的ObjectMonitor,并将Mark Word更新为指向该Monitor的指针。通过这个过程实现java对象(锁对象)与Monitor对象的关联。
总结
本文深入分析了synchronized底层实现依赖,分析了JVM ObjectMonitor结构体的组成,最后分析了java对象头的Mark Word组成部分,简要分析了synchronized锁优化过程以及java对象(锁对象)与Monitor对象的关联的过程。synchronized的底层实现涉及的内容很多,就拿对象头Mark Word来讲,其组成部分就有很多项,我们的主要目的还是围绕问题来分析,在此我们不做更多深入的分析。