Java 线程同步-01:Java对象内存布局和Monitor机制

前言

在并发编程的世界中,synchronized关键字是Java开发者最常用的同步工具。但你是否思考过:当一个对象被synchronized锁定时,JVM底层到底发生了什么?为什么对象能"记住"哪个线程持有它的锁?

这一切的秘密,都藏在Java对象的内存布局中:理解对象内存结构,是掌握Java并发编程的基石。在 Java 线程同步机制中,synchronized 是基于 Java 对象头和 Monitor 机制来实现的。

Java对象的三层结构图

在HotSpot JVM中,每个Java对象在堆内存中都包含三个主要部分:对象头、实例数据和对齐填充。对于线程同步而言,我们更多关注的是其中的对象头部分。

graph TB subgraph "Java对象内存布局" A["整体对象内存"] --> B[对象头] A --> C[实例数据] A --> D[对齐填充] B --> E["Mark Word (8字节)"] B --> F["Klass Pointer (4/8字节)"] B --> G["数组长度 (4字节)"] E --> H["无锁: 01"] E --> I["偏向锁: 01"] E --> J["轻量级锁: 00"] E --> K["重量级锁: 10"] E --> L["GC标记: 11"] C --> M[非静态字段] C --> N[父类字段] D --> O["填充到8的倍数"] end style A fill:#f0f8ff style B fill:#e6f7ff style C fill:#f0fff0 style D fill:#fff0f5 style E fill:#fffacd

实例数据和对齐填充:

实例数据比较好理解,就是这个对象本身的一些变量属性,这里不展开介绍。另外,这里的对齐填充,其实是将java对象大小填充到8的倍数,内存对齐的目的主要有两个:

  1. 提高存取效率:对齐的数据可以通过一次内存操作完成读取
  2. 硬件要求:某些CPU架构要求特定类型的数据必须对齐存储

如果不对齐,一个跨越两个内存字的数据需要两次读取操作,性能会显著下降。

Java 对象头

在线程同步中,Java对象头比较重要,因此单独开个章节进行介绍。

什么是Klass Pointer?

Klass Pointer指向存储在方法区(Metaspace)中的Klass对象。这个Klass对象包含了类的所有元数据:

graph TD subgraph "方法区 Metaspace" Klass["Klass对象"] --> Methods["方法表 vtable"] Klass --> Fields["字段表"] Klass --> ConstPool["常量池"] Klass --> SuperKlass["父类Klass"] Klass --> Interfaces["接口表"] Klass --> AccessFlags["访问标志"] Klass --> ClassLoader["类加载器引用"] end subgraph "堆内存" Object["Java对象实例"] --> Header["对象头"] Header --> KP["Klass Pointer"] KP --> Klass end subgraph "元数据用途" Methods --> Use1["支持多态
动态绑定"] 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数组安全访问的关键:

flowchart TD subgraph "数组访问流程" Start["访问 array[index]"] --> Load["加载数组对象"] Load --> GetHeader["获取对象头"] GetHeader --> Extract["提取数组长度"] Extract --> Check{"0 ≤ index < length ?"} Check -->|是| Calculate["计算元素地址
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在不同锁状态下有不同的位分配:

stateDiagram-v2 [*] --> 无锁状态: 对象创建 无锁状态 --> 可偏向状态: 启用偏向锁 可偏向状态 --> 偏向锁状态: 第一个线程获取锁 偏向锁状态 --> 无锁状态: 偏向锁撤销 偏向锁状态 --> 轻量级锁: 其他线程竞争 无锁状态 --> 轻量级锁: 多个线程竞争 可偏向状态 --> 轻量级锁: 多个线程竞争 轻量级锁 --> 无锁状态: 锁释放 轻量级锁 --> 重量级锁: 竞争激烈 无锁状态 --> 重量级锁: 直接竞争激烈 重量级锁 --> 无锁状态: 锁释放 无锁状态 --> GC标记: 可达性分析 重量级锁 --> GC标记: 可达性分析 GC标记 --> [*]: 对象被回收

其中涉及到几种不同状态的锁,对应的Mark Word的详细结构可以参考如下:

flowchart TD subgraph "Mark Word 64位结构详情" direction TB subgraph "🟢 无锁状态 | 锁标志: 01" NL["25bit: unused
(未使用)"] --> 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() 方法并还未被通知的线程。

对应的内存结构图可以参考如下:

graph TB subgraph "Java对象(重量级锁状态)" Obj["Java对象实例"] --> Header["对象头"] Header --> MW["Mark Word"] MW -->|指向| MonitorPtr["ptr_to_monitor"] end subgraph "ObjectMonitor对象" MonitorPtr --> ObjectMonitor["ObjectMonitor实例"] ObjectMonitor --> Fields["关键字段:"] Fields --> Owner["_owner: Thread*
持有锁的线程"] 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 中的线程再次竞争锁。
stateDiagram-v2 [*] --> 空闲状态: Monitor创建 空闲状态 --> 持有状态: 线程获取锁成功 持有状态 --> 空闲状态: 线程释放锁 持有状态 --> 等待状态: 持有锁线程调用wait() 等待状态 --> 阻塞状态: 被notify()唤醒 阻塞状态 --> 持有状态: 重新获取到锁 空闲状态 --> 阻塞状态: 线程竞争锁失败 note right of 空闲状态 _owner = NULL _EntryList = 空 _WaitSet = 空 end note note left of 持有状态 _owner = 当前线程 _recursions ≥ 1 线程处于RUNNABLE状态 end note note right of 阻塞状态 线程在_EntryList中 状态为BLOCKED 等待被唤醒竞争锁 end note note left of 等待状态 线程在_WaitSet中 状态为WAITING/TIMED_WAITING 等待被notify() end note
相关推荐
焗猪扒饭11 小时前
redis stream用作消息队列极速入门
redis·后端·go
树獭非懒11 小时前
AI大模型小白手册|Embedding 与向量数据库
后端·python·llm
IT_陈寒14 小时前
SpringBoot实战:5个让你的API性能翻倍的隐藏技巧
前端·人工智能·后端
梦想很大很大15 小时前
拒绝“盲猜式”调优:在 Go Gin 项目中落地 OpenTelemetry 链路追踪
运维·后端·go
唐叔在学习15 小时前
就算没有服务器,我照样能够同步数据
后端·python·程序员
用户685453759776916 小时前
同步成本换并行度:多线程、协程、分片、MapReduce 怎么选才不踩坑
后端
javaTodo16 小时前
Claude Code 记忆机制详解:从 CLAUDE.md 到 Auto Memory,六层体系全拆解
后端
LSTM9716 小时前
使用 C# 和 Spire.PDF 从 HTML 模板生成 PDF 的实用指南
后端
JaguarJack16 小时前
为什么 PHP 闭包要加 static?
后端·php·服务端