关于轻量级锁的一些问题

之前研究了下 synchronized 的轻量级锁实现源码,在 轻量级锁加锁&解锁过程 这篇文章里记录了加锁和解锁过程中的流程,本文主要聊聊与之相关的一些问题。

偏向锁、轻量级锁、重量级锁针对的场景

在 synchronized 的实现里,早期的 jdk 版本只有重量级锁一种实现方式,本质上是通过操作系统的 mutex lock 实现的,加锁时需要进行用户态和内核态的切换,成本比较高。针对线程间竞争不那么激烈的情况,jdk 当前已引入了偏向锁和轻量级锁。

锁状态25 bit4bit1bit2bit
23bit2bit是否是偏向锁锁标志位
无锁对象的hashCode对象分代年龄001
偏向锁线程IDEpoch对象分代年龄101
轻量级锁指向栈中锁记录的指针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);
  1. 锁膨胀时,如果锁状态为 INFLATING 或者尝试修改锁状态为 INFLATING 失败,则会进行循环尝试
  2. 重量级锁加锁时,先尝试 cas 获取锁,如果失败再自旋获取锁,最后才会进入阻塞队列等待
1
2
3
4
5
6
7
8
9
10
11
12
// Try one round of spinning *before* enqueueing Self
// and before going through the awkward and expensive state
// transitions. The following spin is strictly optional ...
// Note that if we acquire the monitor from an initial spin
// we forgo posting JVMTI events and firing DTRACE probes.
if (Knob_SpinEarly && TrySpin (Self) > 0) {
assert (_owner == Self , "invariant") ;
assert (_recursions == 0 , "invariant") ;
assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;
Self->_Stalled = 0 ;
return ;
}

轻量级锁加锁时的锁标志位是在哪设置的

翻了翻源码,真没找到明确设置锁标志位的地方,按照 mark word 的定义,加锁后是设置过锁标志位的。唯一的操作 mark word 的代码就是设置指向 Lock Record 指针,猜测应该跟这个动作有关。

1
2
3
4
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
TEVENT (slow_enter: release stacklock) ;
return ;
}
锁状态25 bit4bit1bit2bit
23bit2bit是否是偏向锁锁标志位
轻量级锁指向栈中锁记录的指针00

轻量级锁的锁标志位为 00,转换为数字后就是 4 的倍数,比如 0100 表示 4, 1000 表示 8

二进制十进制
0000 01004
0000 10008
0000 110012
末两位均为0是4的倍数

如果 LockRecord 的地址是均为 4 的倍数,那么设置完 LockRecord 地址后,mark word 的末两位值就是 00 了,就是说一个操作完成了两件事。接下来看下 LockRecord 的地址有什么规律没。

如下是 LockRecord 的定义,它内部有一个 _displaced_header 用于在加锁时存储锁对象的 mark word

1
2
3
4
5
6
7
8
class BasicLock VALUE_OBJ_CLASS_SPEC {
friend class VMStructs;
private:
volatile markOop _displaced_header;
public:
void set_displaced_header(markOop header) { _displaced_header = header; }
......
};

BasicLock 本身会作为 BasicObjectLock 的一个属性存在,BasicObjectLock 的定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// A BasicObjectLock associates a specific Java object with a BasicLock.
// It is currently embedded in an interpreter frame.

// Because some machines have alignment restrictions on the control stack,
// the actual space allocated by the interpreter may include padding words
// after the end of the BasicObjectLock. Also, in order to guarantee
// alignment of the embedded BasicLock objects on such machines, we
// put the embedded BasicLock at the beginning of the struct.

class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
friend class VMStructs;
private:
BasicLock _lock; // the lock, must be double word aligned
oop _obj; // object holds the lock;

public:
// Manipulation
oop obj() const { return _obj; }
void set_obj(oop obj) { _obj = obj; }
BasicLock* lock() { return &_lock; }

// Note: Use frame::interpreter_frame_monitor_size() for the size of BasicObjectLocks
// in interpreter activation frames since it includes machine-specific padding.
static int size() { return sizeof(BasicObjectLock)/wordSize; }

// GC support
void oops_do(OopClosure* f) { f->do_oop(&_obj); }

static int obj_offset_in_bytes() { return offset_of(BasicObjectLock, _obj); }
static int lock_offset_in_bytes() { return offset_of(BasicObjectLock, _lock); }
};

它要求 _lock (LockRecord)必须按两个字对齐,这是因为一些机器的控制栈有对齐的限制。

为了实现这个目的,在 BasicObjectLock 的后面会有用于对齐填充的数据,此外 BasicLock 属性还会放在该结构的开始位置。

回到开头的问题,LockRecord 的地址是按照两个字对齐的,一个字是 32 位(4 个字节),两个字就是 64 位( 8个字节),所以它的地址是 8 的倍数,与开头的猜想一致。

总结:JVM 在将 mark word 设置为 LockRecord 的地址时,副作用是将锁标志位一并设置了。那么轻量级锁对应的 mark word 结构就应该如下

image-20200702011337477

说明:以上并不严谨,最简单的一个验证方法就是 debug 下 jvm 中的加锁代码,这个就留待以后有空了再尝试