引言
为了加快程序的运行效率,我们引入了多线程的概念。但是速度提升的同时引来了共享资源安全性的问题。为了保证多线程下程序的准确性,锁机制就是我们需要掌握的一种手段。本文将介绍的就是 synchronized 的实现原理(以 JDK 8 为基础)。
线程安全问题
不是所有程序在多线程下就是非安全的,我们判断一个程序是否是多线程安全的需要有下面三个要素:
多线程
如果程序是单线程的,那么它肯定是安全的,因为没有人跟你竞争。
共享资源
如果资源都是线程独占式的(局部变量),那么也不涉及到安全问题。
非原子性操作
对资源的操作如果是原子性的,那么也不会存在线程安全性问题。
所以只要同时满足以上三种条件,那么该程序一定是非线程安全性的。
synchronized
synchronized 是众多锁中的一种实现方式,我们可以用它对共享资源的操作加锁以实现安全访问。synchronized 存在三种锁类型:
this
指代当前对象实例,比如修饰成员方法时。要想访问当前资源必须持有当前对象的锁。
class
类对象锁,比如修饰静态方法。由于同一个类可以生成多个对象,那么这些对象独享同一把锁。
object
对象锁,比如同步代码块,你可以指定任意对象为当前资源上锁。当然了,如果指定的是 this 或者 class 那么作用等同于上述两者。
Java 对象内存布局
讲解锁实现之前就需要先了解锁的存储,要想了解锁的存储就得知道对象的内存布局:
我们把焦点放到对象头中的 MarkWord 结构,根据操作系统位数的不同,MarkWord 又分为 32 位及 64 位,如下图:
从以上的结构中,我们看出了锁的相关信息了,其中,锁分为无锁、偏向锁、轻量级锁、重量级锁。每个级别它们各自存储的数据结构也有所差异,那也就是说不同的场景对应不同的锁级别。既然我们知道锁是存储在 MarkWord 中的,那么我们就需要看看锁到底是怎么实现的了。
锁的入口
我们知道 .java 文件经过编译之后都会变成 .class 的字节码,这些字节码都会交由 JVM 进行解释执行。而对于 synchronized 而言,它存在 monitorenter、monitorexit 两个指令。如果修饰方法时,那么是不存在以上两条指令的,只是对方法加了一个 flag,标识出当前方法是同步方法。那么它们的入口是否一样呢?
再开始之前,有必要先了解几个基础概念:
markWord
C++ 中的 markWord 对象是 markOopDesc,这里我们需要记住锁的几种状态。
1 | /** |
BasicLock
获取锁时,会将对象头中的 markword 复制一份无锁版保留下来。解锁时,再替换回去,用于还原锁状态。
1 | /** |
BasicObjectLock
该对象维护锁与目标对象的关联关系。
1 | /** |
偏向锁
偏向锁,顾名思义就是偏向某一方的锁。从 markWord 结构中,我们可以看到当对象处于偏向锁模式时,MarkWord 由线程 ID、Epoch、对象年龄、是否偏向锁、锁标志几个字段组成。
如果一个线程能够将自身的线程 id 保存到 markWord 中,同时将偏向标志打开,就说明该线程成功获取了偏向锁。当线程下次再进来时,跟 markword 中的线程 id 比较,如果一致,说明当前线程已经拥有了锁,那就不需要再次获取锁。
有了以上的基础,我们就开始吧:
1 | /** |
以上就是同步代码块、同步方法的入口点。程序进入 InterpreterRuntime::monitorenter
方法的条件就是当前线程获取偏向锁失败(注意:此时就已经存在 LockRecord 了,大家可以比较一下偏向锁跟轻量级锁中,LockRecord 的作用是否一致)。我们把以上逻辑以图例的形式表现出来:
同步方法
同步代码块
同步代码块退出指令
如果获取偏向锁失败的话,就需要执行 InterpreterRuntime::monitorenter
方法:
monitorenter
InterpreterRuntime::monitorenter 方法是 monitorenter 指令的入口,该方法通过判断是否启用偏向锁来选择快速进入还是慢进入。
1 | /** |
fast_enter
jdk/hotspot/src/share/vm/runtime/synchronizer.cpp
1 | void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) { |
revoke_and_rebias
jdk/hotspot/src/share/vm/runtime/biasedLocking.cpp
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132/**
* Handle 内部维护了 oop 对象,同时 Handle 重写了 -> 操作符,所以
* obj->mark()、obj->klass() 其实调用的是 oop(实际上是 oopDesc ) 的对应方法。
*/
BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) {
// 只有在非安全点的时候才能继续执行
assert(!SafepointSynchronize::is_at_safepoint(), "must not be called while at safepoint");
// 拿到 markWord 对象
markOop mark = obj->mark();
/*
* 匿名偏向就是当前 markWord 中已打开偏向标志位,但是 threadId 为空
*
* 如果处于匿名偏向状态,但是不允许重新偏向,那么就需要撤销偏向
*/
if (mark->is_biased_anonymously() && !attempt_rebias) {
markOop biased_value = mark;
// 获取一个相同年龄的非偏向对象
markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
// 通过 CAS 操作将 Java 对象中的偏向状态设置成未偏向,以此达到撤销偏向的目的
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark);
// 说明 CAS 成功(如果成功了,说明当前没有线程参与竞争)
if (res_mark == biased_value) {
// 返回已撤销
return BIAS_REVOKED;
}
}
// 当前处于偏向模式
else if (mark->has_bias_pattern()) {
// 获取 Java 对象的类对象
Klass* k = obj->klass();
// 拿到类对象中的 markWord
markOop prototype_header = k->prototype_header();
/*
* 如果此时类的状态为未偏向
*/
if (!prototype_header->has_bias_pattern()) {
markOop biased_value = mark;
// 替换
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(prototype_header, obj->mark_addr(), mark);
assert(!(*(obj->mark_addr()))->has_bias_pattern(), "even if we raced, should still be revoked");
// 撤销成功
return BIAS_REVOKED;
}
/*
* 当前偏向时间戳过期的话说明该对象已经偏向失效了。根据具体需求即 attempt_rebias 来决定是重新偏向还是撤销偏向
*/
else if (prototype_header->bias_epoch() != mark->bias_epoch()) {
// 如果需要重新偏向
if (attempt_rebias) {
assert(THREAD->is_Java_thread(), "");
markOop biased_value = mark;
// 既然是重新偏向,就需要将当前线程记录下来,同时还有对象的年龄及偏向时间
markOop rebiased_prototype = markOopDesc::encode((JavaThread*) THREAD, mark->age(), prototype_header->bias_epoch());
// CAS 更新
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(rebiased_prototype, obj->mark_addr(), mark);
if (res_mark == biased_value) {
// 撤销成功并且已重新偏向当前线程
return BIAS_REVOKED_AND_REBIASED;
}
} else {
// 如果只是单纯的撤销偏向
markOop biased_value = mark;
// 那就生成一个相同年龄非偏向的新对象头
markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark);
if (res_mark == biased_value) {
// 撤销偏向成功
return BIAS_REVOKED;
}
}
}
}
// 如果对象头为不可偏向、CAS 更新失败就会进行启发式更新,该方法会对撤销偏向锁计数进行递增(注意这里重写的括号操作符 obj())
HeuristicsResult heuristics = update_heuristics(obj(), attempt_rebias);
// 不可偏向
if (heuristics == HR_NOT_BIASED) {
return NOT_BIASED;
}
// 未到批处理阈值
else if (heuristics == HR_SINGLE_REVOKE) {
// 获取类对象
Klass *k = obj->klass();
// 取类对象中的头信息
markOop prototype_header = k->prototype_header();
// 如果当前线程就是偏向锁的所有者,同时偏向还有效,那就由当前线程完成撤销
if (mark->biased_locker() == THREAD &&
prototype_header->bias_epoch() == mark->bias_epoch()) {
ResourceMark rm;
// 信息记录
if (TraceBiasedLocking) {
tty->print_cr("Revoking bias by walking my own stack:");
}
// 注意 obj() 返回的是内部的 oop 对象,也就是当前对应的 Java 对象
BiasedLocking::Condition cond = revoke_bias(obj(), false, false, (JavaThread*) THREAD);
// 因为已经撤销了,那就把当前线程拥有的 monitor 对象置 NULL
((JavaThread*) THREAD)->set_cached_monitor_info(NULL);
assert(cond == BIAS_REVOKED, "why not?");
return cond;
} else {
// 如果当前线程不是偏向锁偏向的线程,那么就由虚拟机线程处理了。
VM_RevokeBias revoke(&obj, (JavaThread*) THREAD);
VMThread::execute(&revoke);
return revoke.status_code();
}
}
// 如果既不是 HR_BULK_REVOKE 也不是 HR_BULK_REBIAS,那不就有问题了?
assert((heuristics == HR_BULK_REVOKE) ||
(heuristics == HR_BULK_REBIAS), "?");
// 只能交由虚拟机线程完成批处理了)
VM_BulkRevokeBias bulk_revoke(&obj, (JavaThread*) THREAD,
(heuristics == HR_BULK_REBIAS),
attempt_rebias);
VMThread::execute(&bulk_revoke);
return bulk_revoke.status_code();
}JVM 内部为每个类维护了一个偏向锁 revoke 计数器,记录每个对象的撤销次数。当这个计数达到指定阈值(BiasedLockingBulkRebiasThreshold, 20)时,JVM 认为该类的偏向锁有问题了,需要重新偏向。批量操作的含义就是将该类的所有对象进行重偏向(bulk rebias)。
当 bulk rebias 时,会对这个类的 epoch 加一,后期为该类分配对象时都以该值为基础,同时还要对当前已获得偏向锁的 epoch 加一,并将这些锁记录保存在方法栈里。
判断一个对象是否是获得偏向锁的条件是:markWord 后三位为 101,线程 ID 字段等于当前线程,epoch 与该对象所属类的 epoch 相同。如果 epoch 不一样,说明进行了重偏向,只不过没有更新到该对象中,所以也是无效的,即使线程 ID 一样。
如果该类的 revoke 计数继续增加达到另一个阈值(BiasedLockingBulkRevokeThreshold, 40),那就说明该类不再适合偏向,需要进行 bulk revoke 了。如以下代码:
update_heuristics
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
48static HeuristicsResult update_heuristics(oop o, bool allow_rebias) {
// 获取 markWord 对象
markOop mark = o->mark();
// 未处于偏移模式
if (!mark->has_bias_pattern()) {
return HR_NOT_BIASED;
}
// 获取类对象
Klass* k = o->klass();
jlong cur_time = os::javaTimeMillis();
// 最近一次批量撤销偏向锁的时间
jlong last_bulk_revocation_time = k->last_biased_lock_bulk_revocation_time();
// 获取撤销次数
int revocation_count = k->biased_lock_revocation_count();
/*
* BiasedLockingBulkRebiasThreshold = 20
* BiasedLockingBulkRevokeThreshold = 40
* BiasedLockingDecayTime = 25000ms
*/
if ((revocation_count >= BiasedLockingBulkRebiasThreshold) &&
(revocation_count < BiasedLockingBulkRevokeThreshold) &&
(last_bulk_revocation_time != 0) &&
(cur_time - last_bulk_revocation_time >= BiasedLockingDecayTime)) {
// 重置撤销次数
k->set_biased_lock_revocation_count(0);
revocation_count = 0;
}
// 如果小于撤销阈值,那么递增
if (revocation_count <= BiasedLockingBulkRevokeThreshold) {
revocation_count = k->atomic_incr_biased_lock_revocation_count();
}
// 满足撤销阈值,那么需要批量撤销
if (revocation_count == BiasedLockingBulkRevokeThreshold) {
return HR_BULK_REVOKE;
}
// 满足重偏向阈值,需要批量重偏向
if (revocation_count == BiasedLockingBulkRebiasThreshold) {
return HR_BULK_REBIAS;
}
// 只是单纯撤销就行了
return HR_SINGLE_REVOKE;
}如果当前处于非安全点并且撤销重偏向成功的话,那么就说明拿到了锁无需进行锁膨胀。但是,如果当前就在安全点,那么就需要执行安全点的撤销逻辑了。
revoke_at_safepoint
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20void BiasedLocking::revoke_at_safepoint(GrowableArray<Handle>* objs) {
// 必须处于安全点
assert(SafepointSynchronize::is_at_safepoint(), "must only be called while at safepoint");
int len = objs->length();
for (int i = 0; i < len; i++) {
oop obj = (objs->at(i))();
// 对撤销操作进行计数,并获取当前撤销类型
HeuristicsResult heuristics = update_heuristics(obj, false);
if (heuristics == HR_SINGLE_REVOKE) {
// 撤销偏向
revoke_bias(obj, false, false, NULL);
} else if ((heuristics == HR_BULK_REBIAS) ||
(heuristics == HR_BULK_REVOKE)) {
// 批量撤销
bulk_revoke_or_rebias_at_safepoint(obj, (heuristics == HR_BULK_REBIAS), false, NULL);
}
}
// 遍历所有线程,逐步清除缓存的 monitor 信息
clean_up_cached_monitor_info();
}原本计划是用一篇文章将 synchronized 这块核心知识点讲完,但是既然写了,那就从头至尾详详细细的梳理一遍。当然,换来的就是文章篇幅过长,既然如此,那干脆将 synchronized 这块内容分为两篇吧。