偏向锁、轻量级锁和重量级锁

JVM的MarkWord存储结构

:最后两位为锁标记位,倒数第三位是偏向标记,如果是1表示是偏向锁;合并单元格的位数就是 该字段的位数,例如hash code共25(23+2)位。

另外,对于偏向锁,如果Thread ID = 0,表示未加锁

Synchronized锁升级:偏向锁 → 轻量级锁 → 重量级锁

Synchronized 会从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁,这里的轻量级锁就是一种自旋锁

​ 锁只能按照上述的顺序进行升级操作,锁只要升级之后,就不能降级

偏向锁

​ 初次执行到 Synchronized 代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是 ”偏向于第一个获得它的线程“ 的锁

​ 偏向锁是 JDK 默认启动的选项,可以通过 -XX:-UseBiasedLocking 来关闭偏向锁。另外偏向锁默认不是立即就启动的,在程序启动后,通常有几秒的延迟,可以通过命令 -XX:BiasedLockingStartupDelay=0 来关闭延迟

​ 执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高

引入偏向锁的目的是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。偏向锁在无竞争的情况下会把整个同步都消除掉

偏向锁的加锁

​ 如果 JVM 支持偏向锁,那么在分配对象时,分配一个可偏向而未偏向的对象(Mark Word的最后3位 为101,并且Thread ID字段的值为0)

​ 然后,当一个线程访问同步块并获取锁时,将通过 CAS(Compare And Swap) 来尝试将对象头中的 Thread ID字段设置为自己的线程号,如果设置成功,则获得锁,那么以后线程再次进入和退出 同步块时,就不需要使用 CAS 来获取锁,只是简单的测试一个对象头中的Mark Word字段中是否存储着指向当前线程的偏向锁

​ 如果使用 CAS 设置失败时,说明存在锁的竞争,那么将执行偏向锁的撤销操作 (revoke bias),将偏向锁升级为轻量级锁

偏向锁升级轻量级锁

​ 当线程1访问代码块并获取锁对象时,会在 Java对象头和栈帧中记录偏向的锁的 Thread ID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的 Thread ID 和Java对象头中的 Thread ID是否一致,如果一致(还是线程1获取锁对象),则无需使用 CAS 来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的 Thread ID

那么需要查看 Java对象头中记录的线程1是否存活:

  • 如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁
  • 如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程

简单来说:线程A第一次执行完同步代码块后,当线程B尝试获取锁的时候,发现是偏向锁,会判断线程A是否仍然存活

  • 如果线程A仍然存活,将线程A暂停,此时偏向锁升级为轻量级锁,之后线程A继续执行,线程B自旋(自旋超过一定的次数后,会膨胀成重量级锁)
  • 但是如果判断结果是线程A不存在了,则线程B持有此偏向锁,锁不升级
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
static BiasedLocking::Condition revoke_bias(oop obj, bool allow_rebias,
bool is_bulk, JavaThread* requesting_thread) {
markOop mark = obj->mark();
// 检查是否可偏向
if (!mark->has_bias_pattern()) {
return BiasedLocking::NOT_BIASED;
}
uint age = mark->age();
markOop biased_prototype = markOopDesc::biased_locking_prototype()->set_age(age);
markOop unbiased_prototype = markOopDesc::prototype()->set_age(age);
JavaThread* biased_thread = mark->biased_locker();
if (biased_thread == NULL) {
// 可偏向但是未偏向的情况
// 可能的使用场景为:因计算hash code而撤销偏向
if (!allow_rebias) {
obj->set_mark(unbiased_prototype);
}
return BiasedLocking::BIAS_REVOKED;
}
// 判断对象现在偏向的线程是否还存在
// 即对象头中Mark Word中Thread ID字段指向的线程是否存在
bool thread_is_alive = false;
if (requesting_thread == biased_thread) {
// 请求的线程拥有偏向锁
thread_is_alive = true;
} else {
// 请求的线程不拥有偏向锁,递归查询
for (JavaThread* cur_thread = Threads::first();
cur_thread != NULL; cur_thread = cur_thread->next()) {
if (cur_thread == biased_thread) {
thread_is_alive = true;
break;
}
}
}
if (!thread_is_alive) {
if (allow_rebias) {
//退回可偏向但未偏向的状态
obj->set_mark(biased_prototype);
} else {
//偏向撤销,变为无锁状态
obj->set_mark(unbiased_prototype);
}
return BiasedLocking::BIAS_REVOKED;
}
// 拥有偏向锁的线程仍然存活
// 检查该线程是否拥有锁:
// 如果拥有锁,那么需要升级为轻量级锁,然后将displaced mark word复制到线程栈中;
// 如果不再拥有锁,如果允许重偏向,那么将mark word中的Thread ID 重新置0;
// 如果不允许重偏向,那么将mark work设置为无锁状态,即最后两位为01

// cached_monitor_info 是该线程拥有的锁对象的信息,按照从加锁顺序的逆序排列
GrowableArray<MonitorInfo*>* cached_monitor_info =
get_or_compute_monitor_info(biased_thread);
BasicLock* highest_lock = NULL;
for (int i = 0; i < cached_monitor_info->length(); i++) {
MonitorInfo* mon_info = cached_monitor_info->at(i);
if (mon_info->owner() == obj) {
// Assume recursive case and fix up highest lock later
markOop mark = markOopDesc::encode((BasicLock*) NULL);
highest_lock = mon_info->lock();
highest_lock->set_displaced_header(mark);
}
}
if (highest_lock != NULL) {
// 线程拥有锁
// Fix up highest lock to contain displaced header and point
// object at it
highest_lock->set_displaced_header(unbiased_prototype);
// Reset object header to point to displaced mark.
// Must release storing the lock address for platforms without TSO
// ordering (e.g. ppc).
obj->release_set_mark(markOopDesc::encode(highest_lock));
} else {
// 线程不再拥有锁
if (allow_rebias) {
obj->set_mark(biased_prototype);
} else {
obj->set_mark(unbiased_prototype);
}
}
return BiasedLocking::BIAS_REVOKED;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//针对上面的伪代码实现
// 撤销流程的伪代码,在全局检查点执行该操作
if mark word 存储的不是可偏向状态:
return; // 如果不是偏向锁,那么没有撤销偏向的必要
else:
if Thread ID 指向的线程不存活:
if 允许重偏向:
退回可偏向但未偏向的状态 // Thread ID为0
else:
偏向撤销,变为无锁状态
else:
if Thread ID 指向的线程,仍然拥有锁:
升级为轻量级锁,将mark word复制到线程栈中,然后stack pointer指向最老的相关锁记录
else:
if 允许重偏向:
退回可偏向但未偏向的状态 // Thread ID为0
else:
偏向撤销,变为无锁状态

小结:

​ 撤销偏向的操作需要在全局检查点执行。我们假设线程A曾经拥有锁(不确定是否释放锁), 线程B来竞争锁对象,如果当线程A不在拥有锁时或者死亡时,线程B直接去尝试获得锁(根据是否允许重偏向(rebiasing),获得偏向锁或者轻量级锁);如果线程A仍然拥有锁,那么锁升级为轻量级锁,线程B自旋请求获得锁

偏向锁撤销流程

轻量级锁

​ 轻量级锁不是使用操作系统互斥量来实现锁, 而是通过 CAS 操作来实现锁。当线程获得轻量级锁后,可以再次进入锁,即锁是可重入(Reentrance Lock)的

在轻量级锁的加锁阶段,如果线程发现对象头中Mark Word已经存在指向自己栈帧的指针,即线程已经获得轻量级锁,那么只需要将0存储在自己的栈帧中(此过程称为递归加锁);在解锁的时候,如果发现锁记录的内容为0, 那么只需要移除栈帧中的锁记录即可,而不需要更新Mark Word

​ 在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋(即不停地循环判断锁是否能够被成功获取)

忙等

​ 长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)

​ 如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么 Synchronized 就用轻量级锁,允许短时间的忙等现象。短时间的忙等,换取线程在用户态和内核态之间切换的开销

轻量级锁加锁

  1. 线程在执行同步块之前,JVM 会先在当前的线程的栈帧中创建所记录的空间,用于存储对象头中的 Mark Word的拷贝
  2. 然后线程尝试使用 CAS 将对象头中的Mark Word替换为指向锁记录(Lock Record)的指针
  3. 如果成功,当前线程获得轻量级锁
  4. 如果失败,虚拟机先检查当前对象头的Mark Word 是否指向当前线程的栈帧
    • 如果指向,则说明当前线程已经拥有这个对象的锁,则可以直接进入同步块执行操作
    • 否则表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。当竞争线程的自旋次数达到界限值(threshold),轻量级锁将会膨胀为重量级锁

轻量级锁升级重量级锁

重量级锁

​ (有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)

​ 如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是 CAS 修改锁标志位,但不修改持有锁的线程ID)

​ 当后续线程尝试获取锁时,发现被占用的锁是重量级锁,直接进入堵塞状态,此时不消耗CPU,然后等拥有锁的线程释放锁后,唤醒堵塞的线程, 然后线程再次竞争锁

JDK1.6 之前,Synchronized 直接加重量级锁,很明显现在得到了很好的优化

锁竞争

​ 如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争

锁消除

​ 锁消除指的是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间

锁粗化

​ 如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗

​ 如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部

​ 比如说有多个append方法,对每一个方法都加锁,此时会进行锁粗化,将第一个append直到最后一个append包起来,这样就只会进行一次的加锁操作而不是多次

小结

​ 一个锁只能按照 偏向锁、轻量级锁、重量级锁 的顺序逐渐升级(也叫锁膨胀),不允许降级

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,与执行非同步方法仅存在纳秒级的差距 如果线程间存在竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块的情况
轻量级锁 竞争的线程不会堵塞,提高了程序的响应速度 始终得不到锁的线程,使用自旋会消耗CPU 追求响应时间,同步块执行速度非常块,只有两个线程竞争锁
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程堵塞,响应时间缓慢 追求吞吐量,同步块执行速度比较慢,竞争锁的线程大于2个

中断锁

​ Java并没有提供任何直接中断某线程的方法,只提供了中断机制

何谓“中断机制”?

​ 线程A向线程B发出 “请你停止运行” 的请求(线程B也可以自己给自己发送此请求),但线程B并不会立刻停止运行,而是自行选择合适的时机以自己的方式响应中断,也可以直接忽略此中断

​ 也就是说,Java的中断不能直接终止线程,而是需要被中断的线程自己决定怎么处理

​ 如果线程A持有锁,线程B等待获取该锁。由于线程A持有锁的时间过长,线程B不想继续等待了,我们可以让线程B中断自己或者在别的线程里中断它,这种就是可中断锁。在Java中,synchronized就是不可中断锁,而Lock的实现类都是可中断锁