众所周知,让开发者简单轻松的编写保证线程安全的代码,一直是现代编程语言所最求的,Java 也不例外。Java 语言引入的 synchronized 关键字,无不彰显它在此方面的勃勃雄心。但理想丰满现实骨感,早期的 Java 版本里,对于此关键字的实现太过厚重,导致线程同步的性能远不如预期。Java HotSpot™ VM 经过多个版本的迭代,利用锁膨胀思想,尽量延迟使用重量级锁的手段来提升 synchronized 原语的性能。
由于对象在内存以 8 字节 为最小单位,单个对象占用内存字节不是 8 的倍数时,需要增加留空字节凑成倍数,此时留空的字节被称为 对象对齐(object alignment)。单个对象内部的成员变量所占字节不满 4 的倍数时,也需增加留空字节凑成倍数,此时的留空字节被称为 填充间隙(padding gap)。
Mark Word,为运行时对象的标记字,字在 32 位系统中占用 32 bit,64 位操作系统占用 64 bit。其记录着对象运行时的数据,包括 identity_hashcode、GC 分代年龄、锁状态 等信息。以下引用自 JDK8 HotSpot 源码 markOop.hpp 中,关于 Mark Word 结构信息的描述:
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
//
// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
为了节省内存空间,64 位的 JVM 采取压缩普通对象指针 (COOPs,即 Compressed Ordinary Object Pointer) 2 技术,把对象指针由原来的 64 bit 压缩成 32 bit,进而省了一半的内存空间。
我们着重关注 64 位 JVM 的锁的几种状态,也就是通过上面的 biased_lock
、lock
两个标志位的排列组合得到:
biased_lock | lock | 状态 |
---|---|---|
0 | 01 | 无锁 normal object |
1 | 01 | 偏向锁 biased object |
0 | 00 | 轻量级锁 lightweight/thin object |
0 | 10 | 重量级锁 heavyweight/fat object |
0 | 11 | GC 标记 |
根据 Mark Word 中的锁状态,我们分别来介绍下。
利用系统级别的互斥量(mutex)实现同步临界区,由于系统级调用,开销大,故称其位重量级锁。
在下一节关于锁的状态改变图中,会发现一个 重量级监视器指针,由于它覆盖(官方称为 Displaced)了原本的 Mark Word,故它所指向的是复杂的数据结构 ObjectMonitor 就包含有用来存储备份的 Mark Word 信息。除此之外,该数据结构还包含锁的等待列表等信息,详细可参考 Monitor Object 设计模式。
利用循环的方式来实现线程等待(忙等),等待期间内不让出 CPU 执行时间、免去系统级线程切换开销。
采用原子性的 CAS 进行加解锁操作,加锁失败时自旋等待,成功则将 Mark Word 覆盖成指向线程栈中的 Lock Record 指针,此记录中同样含有原 Mark Word 记录的备份信息。CAS 调用不需系统级别调用,故称为轻量级锁。
给当前锁标记所属线程,使得所属线程进入同步临界区不用做任何特殊处理,只是简单的使用 CAS 操作将所属线程 ID 记录到 Mark Word 中,同一线程再次加解锁时无需 CAS 操作。
在给新建的对象分配内存时,其对象头信息会按照下图所示的进行分配,同时随着线程的竞争发送锁状态的转化:
状态转步骤
具体锁转移的过程如下:
-XX:-UseBiasedLocking
可以关闭偏向锁,默认是启用的。-XX:BiasedLockingStartupDelay
默认 4 秒,偏向锁机制会延迟 4 秒生效,测试时可以将其设置为 0 秒,防止偏向锁设置不生效。hashCode()
、 System.identityHashCode(obj)
方法会使偏向锁膨胀为轻量级锁 3。原因为生成的 identity_hashcode
需要被记录到 Mark Word 中,而偏向锁结构没有设计存储备份 Mark Word 的指针,类似轻量级锁的 lock record pointer
、重量级锁的 heavyweight monitor pointer
。001
。<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-cli</artifactId>
<version>0.10</version>
</dependency>
</dependencies>
project
└── src
│ └── test
│ │ └── java
│ │ │ └── org
│ │ │ │ └── reion
│ │ │ │ │ └── LockTest.java
│ └── main
│ │ └── resources
│ │ └── java
│ │ │ └── org
│ │ │ │ └── reion
│ │ │ │ │ └── Student.java
Student.java
LockTest.java
测试方法
输出结果
注意: 由于 JVM 为 小端法(Little Endian) 故低字节在前,高字节在后。
因此:
打印结果中 object header 中 64 位 Mark Word 字节序列应该为
00 00 7f e1 b9 00 a8 05
测试方法
输出结果
测试方法
输出结果
测试方法
输出结果
测试方法
输出结果
测试方法
输出结果
测试方法
输出结果
-XX:-UseCompressedClassPointers
关闭压缩,默认开启。-XX:-UseCompressedOops
关闭压缩,默认开启。source:https://reionchan.github.io/2020/08/26/about-synchronized-lock-optimization/
喜欢,在看