前言
在并发编程的世界中,synchronized关键字是Java开发者最常用的同步工具。但你是否思考过:当一个对象被synchronized锁定时,JVM底层到底发生了什么?为什么对象能"记住"哪个线程持有它的锁?
这一切的秘密,都藏在Java对象的内存布局中:理解对象内存结构,是掌握Java并发编程的基石。在 Java 线程同步机制中,synchronized 是基于 Java 对象头和 Monitor 机制来实现的。
Java对象的三层结构图
在HotSpot JVM中,每个Java对象在堆内存中都包含三个主要部分:对象头、实例数据和对齐填充。对于线程同步而言,我们更多关注的是其中的对象头部分。
实例数据和对齐填充:
实例数据比较好理解,就是这个对象本身的一些变量属性,这里不展开介绍。另外,这里的对齐填充,其实是将java对象大小填充到8的倍数,内存对齐的目的主要有两个:
- 提高存取效率:对齐的数据可以通过一次内存操作完成读取
- 硬件要求:某些CPU架构要求特定类型的数据必须对齐存储
如果不对齐,一个跨越两个内存字的数据需要两次读取操作,性能会显著下降。
Java 对象头
在线程同步中,Java对象头比较重要,因此单独开个章节进行介绍。
什么是Klass Pointer?
Klass Pointer指向存储在方法区(Metaspace)中的Klass对象。这个Klass对象包含了类的所有元数据:
动态绑定"] Fields --> Use2["字段访问
反射操作"] ConstPool --> Use3["符号引用
字面量池"] SuperKlass --> Use4["继承链
类型检查"] end style Klass fill:#d1ecf1 style Object fill:#f9f9f9
什么是数组长度
数组对象在对象头中有一个额外的数组长度字段,这是普通对象所没有的:
java
public class ArrayHeaderDemo {
public static void main(String[] args) {
// 不同类型的数组
int[] intArray = new int[10];
String[] strArray = new String[5];
Object[][] multiArray = new Object[3][4];
// 获取数组长度
System.out.println("int[] length: " + intArray.length); // 存储在数组头
System.out.println("String[] length: " + strArray.length);
System.out.println("Object[][] length: " + multiArray.length);
}
}
数组长度字段是实现Java数组安全访问的关键:
array_start + header + index × size"] Calculate --> Access["安全访问元素"] Check -->|否| Throw["抛出
ArrayIndexOutOfBoundsException"] end style Check fill:#ffcccc style Access fill:#ccffcc style Throw fill:#ffcccc
什么是Mark Word
Mark Word(标记字段)是对象头中最重要也最复杂的部分,它记录了对象运行时的状态信息。Mark Word的结构会根据对象所处的不同状态而变化。Mark Word记录了以下关键信息:
java
public class MarkWordFunctions {
public static void main(String[] args) throws Exception {
Object obj = new Object();
// 1. 哈希码存储
int hashCode = obj.hashCode(); // 第一次调用时计算并存入Mark Word
System.out.println("HashCode: " + hashCode);
// 2. GC分代年龄
// 对象每经历一次Minor GC,年龄加1,达到阈值(默认15)则晋升老年代
// 3. 锁状态信息
synchronized (obj) {
System.out.println("Object is locked");
}
// 4. 偏向锁信息
// 第一个获取锁的线程ID会记录在Mark Word中
}
}
Mark Word在不同锁状态下有不同的位分配:
其中涉及到几种不同状态的锁,对应的Mark Word的详细结构可以参考如下:
(未使用)"] --> HC["31bit: identity_hashcode
(对象哈希码)"] --> NU["1bit: unused
(未使用)"] --> AGE["4bit: age
(GC分代年龄)"] --> BL["1bit: biased_lock: 0
(偏向锁标志)"] --> LOCK["2bit: lock: 01
(锁标志位)"] end subgraph "🟡 偏向锁状态 | 锁标志: 01" TID["54bit: thread
(持有偏向锁的线程ID)"] --> EPOCH["2bit: epoch
(偏向锁时间戳)"] --> BU["1bit: unused
(未使用)"] --> BAGE["4bit: age
(GC分代年龄)"] --> BBL["1bit: biased_lock: 1
(偏向锁标志)"] --> BLOCK["2bit: lock: 01
(锁标志位)"] end subgraph "🔵 轻量级锁状态 | 锁标志: 00" LOCKREC["62bit: ptr_to_lock_record
(指向栈中锁记录的指针)"] --> LLOCK["2bit: lock: 00
(锁标志位)"] end subgraph "🔴 重量级锁状态 | 锁标志: 10" MONITOR["62bit: ptr_to_monitor
(指向Monitor对象的指针)"] --> HLOCK["2bit: lock: 10
(锁标志位)"] end subgraph "⚫ GC标记状态 | 锁标志: 11" GCINFO["62bit: GC信息
(回收相关信息)"] --> GLOCK["2bit: lock: 11
(锁标志位)"] end end style NL fill:#e1f5fe style HC fill:#bbdefb style AGE fill:#c8e6c9 style BL fill:#fff3cd style LOCK fill:#f8d7da style TID fill:#d1c4e9 style EPOCH fill:#b39ddb style BAGE fill:#c8e6c9 style BBL fill:#fff3cd style BLOCK fill:#f8d7da style LOCKREC fill:#ffecb3 style LLOCK fill:#f8d7da style MONITOR fill:#ffcdd2 style HLOCK fill:#f8d7da style GCINFO fill:#cfd8dc style GLOCK fill:#f8d7da
Monitor机制
Monitor是一种同步原语 (Synchronization Primitive),它提供了对共享资源的互斥访问机制。在Java中,每个对象都关联着一个隐式的Monitor。在HotSpot JVM中,Monitor是通过C++的ObjectMonitor类实现的:
cpp
// hotspot/src/share/vm/runtime/objectMonitor.hpp (简化版)
class ObjectMonitor {
public:
// 关键字段
void* volatile _owner; // 当前持有Monitor的线程
volatile intptr_t _recursions; // 锁重入次数
ObjectWaiter* volatile _EntryList; // 等待锁的线程队列
ObjectWaiter* volatile _WaitSet; // 调用wait()等待的线程队列
volatile int _count; // 用于记录线程获取锁的次数
// 方法
void enter(Thread* self); // 获取锁
void exit(Thread* self); // 释放锁
void wait(jlong timeout, bool interruptable, TRAPS); // 等待
void notify(Thread* self); // 通知一个
void notifyAll(Thread* self); // 通知所有
private:
void AddWaiter(ObjectWaiter* waiter); // 添加等待者
void RemoveWaiter(ObjectWaiter* waiter); // 移除等待者
void DequeueWaiter(ObjectWaiter* waiter); // 出队
};
Mark Word 有一个字段指向 monitor 对象。monitor 中记录了锁的持有线程,等待的线程队列等信息。每个对象都有一个锁和一个等待队列,其中有三个关键字段:
- _owner 记录当前持有锁的线程
- _EntryList 是一个队列,记录所有阻塞等待锁的线程
- _WaitSet 也是一个队列,记录调用 wait() 方法并还未被通知的线程。
对应的内存结构图可以参考如下:
持有锁的线程"] Fields --> Recursions["_recursions: int
重入次数"] Fields --> EntryList["_EntryList: ObjectWaiter*
等待锁队列"] Fields --> WaitSet["_WaitSet: ObjectWaiter*
wait等待队列"] Fields --> Count["_count: int
锁计数器"] Owner --> Thread1["线程A (运行中)"] EntryList --> Thread2["线程B (阻塞)"] EntryList --> Thread3["线程C (阻塞)"] WaitSet --> Thread4["线程D (等待)"] WaitSet --> Thread5["线程E (等待)"] end subgraph "线程状态" Thread1 --> State1["RUNNABLE
持有锁,执行中"] Thread2 --> State2["BLOCKED
等待获取锁"] Thread3 --> State3["BLOCKED
等待获取锁"] Thread4 --> State4["WAITING
调用了wait()"] Thread5 --> State5["WAITING
调用了wait()"] end style ObjectMonitor fill:#fff0f0,stroke:#d9534f,stroke-width:2px style Owner fill:#c1e1c1 style EntryList fill:#ffd8b2 style WaitSet fill:#b3e0ff
Monitor的操作机制如下:
- 多个线程竞争锁时,会先进入 EntryList 队列。竞争成功的线程被标记为 Owner。其他线程继续在此队列中阻塞等待。
- 如果 Owner 线程调用 wait() 方法,则其释放对象锁并进入 WaitSet 中等待被唤醒。Owner 被置空,EntryList 中的线程再次竞争锁。
- 如果 Owner 线程执行完了,便会释放锁,Owner 被置空,EntryList 中的线程再次竞争锁。