关于轻量级锁的一些问题
之前研究了下 synchronized
的轻量级锁实现源码,在 轻量级锁加锁&解锁过程 这篇文章里记录了加锁和解锁过程中的流程,本文主要聊聊与之相关的一些问题。
偏向锁、轻量级锁、重量级锁针对的场景
在 synchronized 的实现里,早期的 jdk 版本只有重量级锁一种实现方式,本质上是通过操作系统的 mutex lock 实现的,加锁时需要进行用户态和内核态的切换,成本比较高。针对线程间竞争不那么激烈的情况,jdk 当前已引入了偏向锁和轻量级锁。
锁状态 | 25 bit | 4bit | 1bit | 2bit | |
23bit | 2bit | 是否是偏向锁 | 锁标志位 | ||
无锁 | 对象的hashCode | 对象分代年龄 | 0 | 01 | |
偏向锁 | 线程ID | Epoch | 对象分代年龄 | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 | |||
重量级锁 | 指向互斥量(重量级锁)的指针 | 10 | |||
GC标记 | 空 | 11 |
偏向锁针对的是没有多线程竞争的情况,也就是说只有一个线程访问临界区。它是通过 CAS 操作将加锁线程的线程ID 设置到锁对象的 mark work 中,如果操作成功则表示加锁成功,否则表示存在多个线程竞争,需要升级锁。
轻量级锁针对的是多个线程交替进入同步块的情况。它是通过 CAS 操作将指向加锁线程栈的指针放置在锁对象的 mark word 中,表示已加锁,解锁时再将 mark word 替换回去。加锁和解锁过程中都有可能触发锁膨胀(升级)。
重量级锁针对的是多线程竞争的情况,通过操作系统提供的互斥锁保证同一时刻只有一个线程进入到同步块。
通常项目中加锁的地方存在多线程竞争,此时可通过 XX:-UseBiasedLocking=false
参数关闭偏向锁,避免偏向锁失败的 CAS 操作
哪里有自旋操作
获取轻量级锁失败时会先 1.锁膨胀 再进入 2.重量级锁加锁过程
1 | ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD); |
- 锁膨胀时,如果锁状态为 INFLATING 或者尝试修改锁状态为 INFLATING 失败,则会进行循环尝试
- 重量级锁加锁时,先尝试 cas 获取锁,如果失败再自旋获取锁,最后才会进入阻塞队列等待
1 | // Try one round of spinning *before* enqueueing Self |
轻量级锁加锁时的锁标志位是在哪设置的
翻了翻源码,真没找到明确设置锁标志位的地方,按照 mark word 的定义,加锁后是设置过锁标志位的。唯一的操作 mark word 的代码就是设置指向 Lock Record 指针,猜测应该跟这个动作有关。
1 | if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) { |
锁状态 | 25 bit | 4bit | 1bit | 2bit | |
23bit | 2bit | 是否是偏向锁 | 锁标志位 | ||
轻量级锁 | 指向栈中锁记录的指针 | 00 |
轻量级锁的锁标志位为 00
,转换为数字后就是 4 的倍数,比如 0100
表示 4, 1000
表示 8
二进制 | 十进制 |
---|---|
0000 0100 | 4 |
0000 1000 | 8 |
0000 1100 | 12 |
末两位均为0 | 是4的倍数 |
如果 LockRecord 的地址是均为 4 的倍数,那么设置完 LockRecord 地址后,mark word 的末两位值就是 00
了,就是说一个操作完成了两件事。接下来看下 LockRecord 的地址有什么规律没。
如下是 LockRecord 的定义,它内部有一个 _displaced_header
用于在加锁时存储锁对象的 mark word
1 | class BasicLock VALUE_OBJ_CLASS_SPEC { |
BasicLock
本身会作为 BasicObjectLock
的一个属性存在,BasicObjectLock
的定义如下
1 | // A BasicObjectLock associates a specific Java object with a BasicLock. |
它要求 _lock
(LockRecord)必须按两个字对齐,这是因为一些机器的控制栈有对齐的限制。
为了实现这个目的,在 BasicObjectLock 的后面会有用于对齐填充的数据,此外 BasicLock 属性还会放在该结构的开始位置。
回到开头的问题,LockRecord 的地址是按照两个字对齐的,一个字是 32 位(4 个字节),两个字就是 64 位( 8个字节),所以它的地址是 8 的倍数,与开头的猜想一致。
总结:JVM 在将 mark word 设置为 LockRecord 的地址时,副作用是将锁标志位一并设置了。那么轻量级锁对应的 mark word 结构就应该如下
说明:以上并不严谨,最简单的一个验证方法就是 debug 下 jvm 中的加锁代码,这个就留待以后有空了再尝试
- 2019-03-15
轻量级锁是对 synchronized 的一种优化机制,它是一种乐观锁,适用于多线程竞争比较弱的情况。在这种情况下,相对于传统的重量级互斥锁(使用操作系统互斥量加锁),轻量级锁的性能更好。