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
相关推荐
青云计划15 小时前
知光项目知文发布模块
java·后端·spring·mybatis
Victor35615 小时前
MongoDB(9)什么是MongoDB的副本集(Replica Set)?
后端
Victor35615 小时前
MongoDB(8)什么是聚合(Aggregation)?
后端
yeyeye11117 小时前
Spring Cloud Data Flow 简介
后端·spring·spring cloud
Tony Bai17 小时前
告别 Flaky Tests:Go 官方拟引入 testing/nettest,重塑内存网络测试标准
开发语言·网络·后端·golang·php
+VX:Fegn089518 小时前
计算机毕业设计|基于springboot + vue鲜花商城系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
程序猿阿伟18 小时前
《GraphQL批处理与全局缓存共享的底层逻辑》
后端·缓存·graphql
小小张说故事18 小时前
SQLAlchemy 技术入门指南
后端·python
识君啊18 小时前
SpringBoot 事务管理解析 - @Transactional 的正确用法与常见坑
java·数据库·spring boot·后端
想用offer打牌19 小时前
MCP (Model Context Protocol) 技术理解 - 第五篇
人工智能·后端·mcp