0 前言
之前在学习synchronized时,对偏向锁进行测试的时候遇到一个奇怪的问题,在此记录一下。
1 偏向锁介绍
什么是偏向锁?为什么要有偏向锁?
-
synchronized在1.6 之前只有重量级锁,重量级锁会涉及到线程的挂起和唤醒,底层借助操作系统的互斥锁来实现,有较大的开销,所以1.6之前的synchronized性能是较差的,所以在1.6之后对synchronized进行了优化 -
偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁的代价而引入偏向锁。
-
偏向锁可以简单的从字面意思来理解,意为偏向于某一个线程,如果还是同一个线程来获取资源,则不需要进行上锁,如果是不同的线程来获取资源,则不行。
偏向锁是怎么实现的?
- 加锁:通过 CAS 操作,将当前线程的地址(也当做唯一ID)记录到 markword 中
- 偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的
- 其他线程竞争偏向锁时会尝试通过CAS修改
Mark Word中的线程ID,如果成功则重偏向,如果失败就会升级为轻量级锁
2 问题描述
通过源码分析可以得出一个结论,如果偏向锁一来是是线程1获得,此时线程2想要获得偏向锁时发现两个线程不一样,代表有多个线程获取资源,此时应该升级为轻量级锁。
但是在本人进行测试时发生了奇怪的现象,测试代码如下:
import org.openjdk.jol.info.ClassLayout;
public class TestSync3 {
static Object yesLock = new Object();
public static void main(String[] args) throws InterruptedException {
// 这里休眠5秒是因为jvm在4秒之后才会开启偏向锁
Thread.sleep(5000);
yesLock = new Object();
System.out.println("无锁时对象布局:" + ClassLayout.parseInstance(yesLock).toPrintable());
getLock("偏向锁");
Thread.sleep(3000);
getLock("轻量级锁");
}
private static void getLock(final String expectLockLevel) {
new Thread(() -> {
try {
synchronized (yesLock) {
System.out.println("线程[" + Thread.currentThread().getName() + "]" +
":" + expectLockLevel + "状态对象布局:" + ClassLayout.parseInstance(yesLock).toPrintable());
Thread.sleep(2000);
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
正常测试结果如下:
无锁时对象布局:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
线程[Thread-0]:偏向锁状态对象布局:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000020026005 (biased: 0x0000000000080098; epoch: 0; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
无锁时对象布局:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000020026005 (biased: 0x0000000000080098; epoch: 0; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
线程[Thread-1]:轻量级锁状态对象布局:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000002072f740 (thin lock: 0x000000002072f740)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
异常测试结果如下:
无锁时对象布局:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
线程[Thread-0]:偏向锁状态对象布局:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00000000200e5005 (biased: 0x0000000000080394; epoch: 0; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
无锁时对象布局:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00000000200e5005 (biased: 0x0000000000080394; epoch: 0; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
线程[Thread-1]:轻量级锁状态对象布局:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00000000200e5005 (biased: 0x0000000000080394; epoch: 0; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
- 可以看出,明明是前后两个线程获取
偏向锁,却有时Thread-1仍然能够获取偏向锁
3 原因分析
关于此问题,困扰了我许久,后来在网上求助某位大佬才得到了答案,原文链接如下:
- 偏向锁加锁时,会在Mark Word中写入线程的地址信息
- 这个线程在java语言层面上是 java.lang.Thread 对象,其实就是比较javaThread对象的内存地址
- 所以发生这种情况的原因有可能是前后两个Thread对象的内存地址是一样的
为了证明这个说法,可以在代码中输出当前线程的eetop 的信息
Field etopField = Thread.class.getDeclaredField("eetop");
etopField.setAccessible(true);
Object etop = etopField.get(Thread.currentThread());
System.out.println("线程[" + Thread.currentThread().getName() + "]" + "的eetop: " + etop);
结果如下:
无锁时对象布局:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
线程[Thread-0]:偏向锁状态对象布局:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000001fa9f805 (biased: 0x000000000007ea7e; epoch: 0; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
线程[Thread-0]的eetop: 531232768
无锁时对象布局:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000001fa9f805 (biased: 0x000000000007ea7e; epoch: 0; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
线程[Thread-1]:轻量级锁状态对象布局:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000001fa9f805 (biased: 0x000000000007ea7e; epoch: 0; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
线程[Thread-1]的eetop: 531232768
- 可以看到,确实前后两个Thread对象的地址信息是完全一样的