对象头、Monitor与synchronized

从最开始学Java的时候,便听说int类型比Integer类型要小很多,那么究竟是小在哪里呢?Integerint究竟多了什么东西?这是本文要着重介绍的对象头,并介绍Monitor和锁。

对象头

对象头是Java对象在内存布局中的第一部分,包含了对象的元数据信息。每个Java对象在堆内存中都由对象头实例数据对齐填充三部分组成。

下面以32位虚拟机来介绍对象头的结构:

  1. 普通对象

普通对象的对象头共64位,包括:

  • 32位的Mark Word:最核心的部分,存储了对象的运行时数据
  • 32位的Klass Word:指向方法区中类元数据的指针,JVM通过这个指针确定对象属于哪个类。
  1. 数组对象

数组对象相对于普通对象,多了一个array length字段,用于存放数组声明时的大小。

对象头的作用

对象头的关键是Mark Word字段,因此本节深入探索Mark Word

在正常的情况下,Mark Word的结构如下:

下面来介绍这些字段:

  • hashcode字段是JVM在对象内部维护的一个内部标识 。它是一个31位的整数(在32/64位系统中位置不同),也称为identity hash code。这个值是一个懒加载的,只有当JVM第一次需要用到这个内部哈希码时,才会计算并存入Mark Word:

    • 首次调用默认的Object.hashCode()方法(即没有重写过的)。
    • 首次调用System.identityHashCode(obj)方法
    • 在某些锁升级(如偏向锁)等情况下,也可能被计算。
  • age字段和垃圾回收机制有关

  • biased_lock字段,代表偏向锁是否启用

  • 剩下的两位可以理解为当前的对象锁模式。

Monitor

Monitor是JVM实现的同步机制,包含以下关键组件:

  • Owner:当前持有锁的线程
  • Entry Set:等待获取锁的队列
  • Wait Set:调用wait方法后进入等待状态的线程队列
  • 其他组件:
    • Recursion:记录锁重入的次数
    • Count:线程获取锁的次数

只有在对象作为锁的时候,才会创建一个对应的Monitor

当通过synchronized去对一个对象加锁时,如果该锁不被其他线程持有,将获得这把锁,Owner变成该线程,如果该锁已经被其他线程持有,将会进入EntryList阻塞。

当线程获得锁之后,该对象的Mark Word将从

变成

也就是说,对象头中的Mark word变成了一个指向monitor的指针,而原本的Mark word信息保存在了Monitor中的_header字段中。

锁升级机制

虽然代码中使用synchronized,但是实际上可能根本就不存在多线程竞争资源,因此锁升级机制就出现了。

在 HotSpot JVM 中,对象头的 Mark Word 记录了锁的状态,升级路径主要经过以下几个阶段:

  1. 无锁状态:一个新创建的对象,尚未被任何线程锁定。
  2. 偏向锁 :适用于只有一个线程反复进入同步块的场景。第一次获取锁时,会在对象头和栈帧中记录偏向的线程ID。之后该线程再进入时,只需简单检查线程ID是否匹配,无需任何原子操作(如CAS),开销极低。
  3. 轻量级锁 :当有少量线程交替竞争(即竞争不激烈,没有同时抢)时升级为此状态。它通过CPU的CAS操作在对象头和线程栈帧中复制、替换锁记录来实现同步,避免了操作系统级互斥量(Mutex)的阻塞开销。
  4. 重量级锁 :当有多线程激烈竞争 时,轻量级锁会通过自旋 尝试获取锁。若自旋失败(或达到阈值),锁会膨胀为重量级锁。此时,未获取到锁的线程会被挂起,进入操作系统的等待队列,等待被唤醒。这是开销最大的锁。

偏向锁

当如果一个锁在绝大多数时间里,都被同一个线程反复获得,那么JVM就可以让这个线程后续的加锁操作无需再进行任何同步操作(如CAS原子指令)

为了实现偏向,对象头中的Mark Word会记录第一个获得它的线程的ID ,以及一个偏向时间戳(epoch)

当对象被创建后,如果没有禁用偏向锁,将会在一定时间后,Mark word会从

变成

最开始,thread是0,因为此时还未偏向于任何一个线程。

加锁

当一个线程T1第一次获取这个对象锁后:

  1. JVM检查对象是否处于可偏向状态。
  2. 通过一次CAS操作 ,尝试将当前线程T1id写入对象的Mark Word。
  3. 如果CAS成功thread将记录T1id 。这意味着锁偏向了线程T1。这次操作开销稍大,因为包含了CAS。

当一个线程T1再次进入这个同步块:

  1. JVM检查对象头中的线程id
  2. 发现就是自己(T1),则直接通过,没有任何原子操作或系统调用。

解锁

线程T1执行完同步块,并不会主动释放偏向锁 。它不会将Mark Word中的线程ID清空。对象依然保持在 "已偏向于T1" ​ 的状态。这为T1下次快速进入创造了条件。

锁升级

当有另一个线程T2也试图获取这个锁时:

  1. 撤销(Revoke) :JVM需要"主持公道",撤销对T1的偏向。这个过程必须在全局安全点进行,它会暂停拥有偏向锁的线程T1。

  2. 检查T1状态

    • 如果T1已经退出 同步块,则撤销完成,将对象恢复到 "可偏向状态" (但此时T2还未获得锁)。
    • 如果T1仍在执行 同步块,则将偏向锁升级为轻量级锁

轻量级锁

在大多数情况下,同步代码块在运行期间不存在竞争,如果每次加锁都使用操作系统的重量级互斥量,会造成巨大的性能开销。

加锁

当锁升级为轻量级锁后,一个线程获取锁后,Mark Word

变成

加锁时,会在当前线程的栈帧中,创建一个LOCK RECORD,原本的Mark Word复制到LOCK RECORD中的Displaced Mark Word

其加锁过程如下:

  1. 检查对象头是否是无锁状态
  2. 创建LOCK RECORD
  3. CAS更新

解锁

  1. 检查当前锁是否为轻量级锁(锁标志位=00)
  2. 从对象头获取指向锁记录的指针
  3. 使用CAS将对象头恢复为Displaced Mark Word

锁升级

升级的条件是:

  1. CAS尝试失败 && 自旋次数 > 阈值,JVM默认采用自适应自旋,自旋次数阈值是动态计算的
  2. 第三个线程参与竞争
  3. 调用wait

重量级锁

通过synchronized获取重量级锁时,synchronized会被编译为monitorentermonitorexit指令。

重量锁的"重"和轻量锁的"轻"

重量锁之所以重,是因为它需要依赖操作系统的互斥锁 来实现线程同步,这会涉及到用户态到内核态 的切换,并且线程竞争失败会阻塞,后续获得锁后要切换线程的上下文 ,而且还需要关联一个Monitor

轻量级锁是完全在在用户态 完成的,竞争失败也只是自旋

相关推荐
我爱娃哈哈1 小时前
SpringBoot + Redis Stream + 消费组:替代 Kafka 轻量级消息队列,低延迟高吞吐
后端
程序员大飞哥1 小时前
MPTCP 协议全景:从 RFC 6824 到 RFC 8684 的演进
后端
程序员大飞哥1 小时前
MPTCP 握手全解剖:一条连接是如何"长出"多条腿的
后端
凛訫訫1 小时前
Java基础--面向对象高级(2)
后端
悟空码字1 小时前
滑块拼图验证:SpringBoot完整实现+轨迹验证+Redis分布式方案
java·spring boot·后端
luffy54592 小时前
Rust语言入门-变量篇
开发语言·后端·rust
MegaDataFlowers2 小时前
快速上手Spring
java·后端·spring
小江的记录本2 小时前
【MyBatis-Plus】Spring Boot + MyBatis-Plus 进行各种数据库操作(附完整 CRUD 项目代码示例)
java·前端·数据库·spring boot·后端·sql·mybatis
大傻^2 小时前
Spring AI Alibaba Function Calling:外部工具集成与业务函数注册
java·人工智能·后端·spring·springai·springaialibaba