Java | synchronized 不同情况下的对象头测试
synchronized 不同情况下的对象头测试
测试环境
JDK:Oracle JDK 1.8.0_144
代码依赖:
junit-jupiter-engine:5.8.1
slf4j-simple:1.7.32
jol-core:0.16
测试代码
1import java.util.concurrent.TimeUnit;
2import org.junit.jupiter.api.Assertions;
3import org.junit.jupiter.api.Test;
4import org.openjdk.jol.info.ClassLayout;
5import org.slf4j.Logger;
6import org.slf4j.LoggerFactory;
7
8class LockObject {}
9
10class SyncTest {
11
12 private static final Logger log = LoggerFactory.getLogger(SyncTest.class);
13
14 @Test
15 void testSynchronizedLock() throws InterruptedException {
16 Object lock = new Object();
17 syncLock(lock);
18 Assertions.assertTrue(true);
19 }
20
21 void syncLock(Object lock) {
22 log.info("加锁前 {}", ClassLayout.parseInstance(lock).toPrintable());
23 synchronized (lock) {
24 log.info("加锁中 {}", ClassLayout.parseInstance(lock).toPrintable());
25 }
26 log.info("加锁后 {}", ClassLayout.parseInstance(lock).toPrintable());
27 }
28}
测试情况
这里通过改动 testSynchronizedLock
方法代码进行测试,下面的测试情况只说明改动后的 testSynchronizedLock
的代码,其余代码不再说明。因为只关注对象头的变化,其余的值也省略了。
情况一:同线程直接调用
1void testSynchronizedLock() throws InterruptedException {
2 Object lock = new Object();
3 syncLock(lock);
4 Assertions.assertTrue(true);
5}
执行结果为:
1加锁前 0x0000000000000001 (non-biasable; age: 0)
2加锁中 0x0000700007830f10 (thin lock: 0x0000700007830f10)
3加锁后 0x0000000000000001 (non-biasable; age: 0)
通过结果可以看到,加锁前的对象头是 0x0000000000000001,加锁中是 0x0000700007830f10,加锁后是 0x0000000000000001。看着可能不太明白,这里简单说下 64 位 jvm 的对象头的分布情况
1|--------------------------------------------------------------------------------------------------------------|
2| Object Header (128 bits) |
3|--------------------------------------------------------------------------------------------------------------|
4| Mark Word (64 bits) | Klass Word (64 bits) |
5|--------------------------------------------------------------------------------------------------------------|
6| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 无锁
7|----------------------------------------------------------------------|--------|------------------------------|
8| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 偏向锁
9|----------------------------------------------------------------------|--------|------------------------------|
10| ptr_to_lock_record:62 | lock:2 | OOP to metadata object | 轻量锁
11|----------------------------------------------------------------------|--------|------------------------------|
12| ptr_to_heavyweight_monitor:62 | lock:2 | OOP to metadata object | 重量锁
13|----------------------------------------------------------------------|--------|------------------------------|
14| | lock:2 | OOP to metadata object | GC
15|--------------------------------------------------------------------------------------------------------------|
16
lock: 锁状态标记位,该标记的值不同,整个mark word表示的含义不同。
biased_lock:偏向锁标记,为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
从分布可以得出,看锁标记,直接看后 3 位即可
biased_lock | lock | 16进制 | 状态 |
---|---|---|---|
0 | 01 | 1 | 无锁 |
1 | 01 | 5 | 偏向 |
0 | 00 | 0 | 轻量 |
0 | 10 | 2 | 重量 |
0 | 11 | 3 | GC |
加锁前的对象头是 0x0000000000000001,加锁中是 0x0000700007830f10,加锁后是 0x0000000000000001
从这种情况可以看出:加锁前对象处于无锁状态,加锁中处于轻量锁状态,释放锁后处于无锁状态
这种现象和我们想象的可能不太一样,在网上找了资料如下:
JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量 synchronized 关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延时加载偏向锁。这个延时的时间大概为 4s 左右,具体时间因机器而异。当然我们也可以设置 JVM 参数 -XX:BiasedLockingStartupDelay=0 来取消延时加载偏向锁。
从上面可以看出,JVM默认延时加载偏向锁,时间大于 4s,为了更好的验证,下面的代码直接按 10s 处理。
情况二:先获取一次锁,然后延迟 10s 再次获取
这个主要为了验证一下上面的结论
1void testSynchronizedLock() throws InterruptedException {
2 Object lock = new Object();
3 syncLock(lock);
4 TimeUnit.SECONDS.sleep(10);
5 syncLock(lock);
6 Assertions.assertTrue(true);
7}
日志输出如下:
1加锁前 0x0000000000000001 (non-biasable; age: 0)
2加锁中 0x00007000028aaf10 (thin lock: 0x00007000028aaf10)
3加锁后 0x0000000000000001 (non-biasable; age: 0)
4
5加锁前 0x0000000000000001 (non-biasable; age: 0)
6加锁中 0x00007000028aaf10 (thin lock: 0x00007000028aaf10)
7加锁后 0x0000000000000001 (non-biasable; age: 0)
两次获取锁都使用的轻量级锁
情况三:延迟 10s 后在创建锁对象后调用
1void testSynchronizedLock() throws InterruptedException {
2 TimeUnit.SECONDS.sleep(10);
3 Object lock = new Object();
4 syncLock(lock);
5 Assertions.assertTrue(true);
6}
日志如下:
1加锁前 0x0000000000000005 (biasable; age: 0)
2加锁中 0x00007fb114010805 (biased: 0x0000001fec450042; epoch: 0; age: 0)
3加锁后 0x00007fb114010805 (biased: 0x0000001fec450042; epoch: 0; age: 0)
从这种情况可以看出:加锁前对象处于偏向锁状态,加锁中处于偏向锁状态,释放锁后处于偏向锁状态,不过在加锁前,并没有偏向任何线程
情况四:增加 BiasedLockingStartupDelay=0 参数
1void testSynchronizedLock() throws InterruptedException {
2 Object lock = new Object();
3 syncLock(lock);
4 Assertions.assertTrue(true);
5}
日志
1加锁前 0x0000000000000005 (biasable; age: 0)
2加锁中 0x00007fd650009005 (biased: 0x0000001ff5940024; epoch: 0; age: 0)
3加锁后 0x00007fd650009005 (biased: 0x0000001ff5940024; epoch: 0; age: 0)
从这种情况可以看出:加锁前对象处于偏向锁状态,加锁中处于偏向锁状态,释放锁后处于偏向锁状态,不过在加锁前,并没有偏向任何线程
从上述四种情况可以得出:
默认情况 JVM 会延迟启动偏向锁功能,在 JVM 启用偏向锁功能前创建的锁对象,直接使用轻量级锁开始获取锁,而不会通过轻量级锁阶段。如果关闭延迟功能,可以使用 -XX:BiasedLockingStartupDelay=0
参数
后面的测试情况使用 TimeUnit.SECONDS.sleep(10); 来实现和
-XX:BiasedLockingStartupDelay=0
的效果
情况五:同线程多次调用
1void testSynchronizedLock() throws InterruptedException {
2 TimeUnit.SECONDS.sleep(10);
3 Object lock = new Object();
4 syncLock(lock);
5 syncLock(lock);
6 Assertions.assertTrue(true);
7}
日志如下:
1加锁前 0x0000000000000005 (biasable; age: 0)
2加锁中 0x0000023099602005 (biased: 0x000000008c265808; epoch: 0; age: 0)
3加锁后 0x0000023099602005 (biased: 0x000000008c265808; epoch: 0; age: 0)
4
5加锁前 0x0000023099602005 (biased: 0x000000008c265808; epoch: 0; age: 0)
6加锁中 0x0000023099602005 (biased: 0x000000008c265808; epoch: 0; age: 0)
7加锁后 0x0000023099602005 (biased: 0x000000008c265808; epoch: 0; age: 0)
从日志可以看出,第一次加锁时,使用的偏向锁,加锁后偏向于 0x000000008c265808
第二次加锁时,因为还在同一线程内,偏向锁指向还是一样,则直接获取锁,不进行锁升级。
情况六:多线程无竞争两次调用
1void testSynchronizedLock() throws InterruptedException {
2 TimeUnit.SECONDS.sleep(10);
3 Object lock = new Object();
4 syncLock(lock);
5 Thread thread = new Thread(() -> syncLock(lock));
6 thread.start();
7 thread.join();
8 Assertions.assertTrue(true);
9}
日志如下:
1加锁前 0x0000000000000005 (biasable; age: 0)
2加锁中 0x00000264e4573005 (biased: 0x00000000993915cc; epoch: 0; age: 0)
3加锁后 0x00000264e4573005 (biased: 0x00000000993915cc; epoch: 0; age: 0)
4
5加锁前 0x00000264e4573005 (biased: 0x00000000993915cc; epoch: 0; age: 0)
6加锁中 0x000000d0c6dff748 (thin lock: 0x000000d0c6dff748)
7加锁后 0x0000000000000001 (non-biasable; age: 0)
从日志可以看出,第一次加锁时,使用的偏向锁,第二次加锁时使用的轻量级锁(8的二进制时 1000),从中可以得出,即使没有竞争关系,只要有一个线程加过锁,那另一个线程再加锁就会变成轻量级锁,从最后一次日志可以看出,最终又变成了无锁状态
情况七:多线程无竞争很多次调用
1void testSynchronizedLock() throws InterruptedException {
2 TimeUnit.SECONDS.sleep(10);
3 Object lock = new Object();
4 syncLock(lock);
5 Thread thread = new Thread(() -> syncLock(lock));
6 thread.start();
7 thread.join();
8 syncLock(lock);
9 thread = new Thread(() -> syncLock(lock));
10 thread.start();
11 thread.join();
12 Assertions.assertTrue(true);
13}
日志
1加锁前 0x0000000000000005 (biasable; age: 0)
2加锁中 0x0000028212c72005 (biased: 0x00000000a084b1c8; epoch: 0; age: 0)
3加锁后 0x0000028212c72005 (biased: 0x00000000a084b1c8; epoch: 0; age: 0)
4
5加锁前 0x0000028212c72005 (biased: 0x00000000a084b1c8; epoch: 0; age: 0)
6加锁中 0x000000ca803fefd8 (thin lock: 0x000000ca803fefd8)
7加锁后 0x0000000000000001 (non-biasable; age: 0)
8
9加锁前 0x0000000000000001 (non-biasable; age: 0)
10加锁中 0x000000cafeefc8a0 (thin lock: 0x000000cafeefc8a0)
11加锁后 0x0000000000000001 (non-biasable; age: 0)
12
13加锁前 0x0000000000000001 (non-biasable; age: 0)
14加锁中 0x000000ca803ff308 (thin lock: 0x000000ca803ff308)
15加锁后 0x0000000000000001 (non-biasable; age: 0)
结果就是验证了,轻量级锁是可以转换成无锁的
情况八:多线程有竞争调用
1void testSynchronizedLock() throws InterruptedException {
2 TimeUnit.SECONDS.sleep(10);
3 Object lock = new Object();
4 syncLock(lock);
5 Thread thread = new Thread(() -> syncLock(lock));
6 Thread thread2 = new Thread(() -> syncLock(lock));
7 thread.start();
8 thread2.start();
9
10 thread.join();
11 thread2.join();
12 Assertions.assertTrue(true);
13}
14
15void syncLock(Object lock) {
16 log.info("currentThread {}", Thread.currentThread().getId());
17 log.info("加锁前 {}", ClassLayout.parseInstance(lock).toPrintable());
18 synchronized (lock) {
19 try {
20 TimeUnit.SECONDS.sleep(10);
21 } catch (InterruptedException e) {
22 e.printStackTrace();
23 }
24 log.info("加锁中 {}", ClassLayout.parseInstance(lock).toPrintable());
25 }
26 log.info("加锁后 {}", ClassLayout.parseInstance(lock).toPrintable());
27}
日志如下:
1加锁前 0x0000000000000005 (biasable; age: 0)
2加锁中 0x000001b16e421005 (biased: 0x000000006c5b9084; epoch: 0; age: 0)
3加锁后 0x000001b16e421005 (biased: 0x000000006c5b9084; epoch: 0; age: 0)
4
5加锁前 0x000001b16e421005 (biased: 0x000000006c5b9084; epoch: 0; age: 0)
6加锁前 0x000001b16e421005 (biased: 0x000000006c5b9084; epoch: 0; age: 0)
7加锁中 0x000001b10b4f0fba (fat lock: 0x000001b10b4f0fba)
8加锁后 0x000001b10b4f0fba (fat lock: 0x000001b10b4f0fba)
9加锁中 0x000001b10b4f0fba (fat lock: 0x000001b10b4f0fba)
10加锁后 0x000001b10b4f0fba (fat lock: 0x000001b10b4f0fba)
从日志可以看出,显示偏向,然后是重量级锁,最后没有变成无锁