synchronized 原理

本文是对于 死磕Synchronized底层实现 系列的个人补充

优秀博文推荐

关于 synchronized 原理的解析众说纷纭,尤其是 jdk1.6 以后引入的相关锁优化的内容。

远古巨著 Java Concurrency in Practice 出版时间较早,因此没有锁优化的内容讲解。

:star::star::star: 深入理解 Java 虚拟机(第 3 版) 最后一个章节对锁优化的内容做了讲解,并给出了一张经典的关系图,如下:

但是毕竟是 JVM 的书籍,锁优化内容的讲解相对来说还是较简略的。

下面是在网络上找到的一些讲解 synchronized 原理的优秀博文:

  • :star::star::star::star::star: 死磕Synchronized底层实现:该系列总共有四篇文章,基于 Hotspot jdk8u 源码讲解,至少第一篇概论是需要完全搞懂的
  • :star::star::star: 难搞的偏向锁终于被 Java 移除了:适合新手入门的讲解,不涉及 Hotspot jdk8u 的源码。

    关于批量撤销一节,原文中如下内容存在问题:

    如果在距离上次批量重偏向发生超过 25 秒之外,那么就会重置在 [20, 40) 内的计数, 再给次机会

    Hotspot Jdk8u 版本下,如果批量重偏向发生后的 25 秒内,偏向撤销次数没有从 20 达到批量撤销阈值 40,此时不是重置 [20, 40) 内的计数(即重置到 20),而是直接重置为 0,这意味着批量重偏向是可以再次发生的!

    Hotspot Jdk8u 源码 如下:

    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
    enum HeuristicsResult {
    HR_NOT_BIASED = 1, // 锁对象偏向模式关闭的情况:无需偏向撤销
    HR_SINGLE_REVOKE = 2, // 单次偏向撤销
    HR_BULK_REBIAS = 3, // 批量重偏向
    HR_BULK_REVOKE = 4 // 批量撤销
    };

    // 递增锁对象的 klass 中的偏向撤销计数,并返回结果
    static HeuristicsResult update_heuristics(oop o, bool allow_rebias) {
    markOop mark = o->mark();
    if (!mark->has_bias_pattern()) {
    // 锁对象偏向模式关闭的情况
    return HR_NOT_BIASED;
    }

    // 获取对象的 class 信息
    Klass* k = o->klass();
    // 获得当前时间
    jlong cur_time = os::javaTimeMillis();
    // 获得上次批量重偏向的时间
    jlong last_bulk_revocation_time = k->last_biased_lock_bulk_revocation_time();
    // 获取 class 中记录的偏向撤销次数
    int revocation_count = k->biased_lock_revocation_count();
    // 如果 偏向撤销次数 >= 批量重偏向阈值 且
    // 偏向撤销次数 < 批量撤销阈值 且
    // 距离上次批量重偏向的时间超过了指定时间
    if ((revocation_count >= BiasedLockingBulkRebiasThreshold) &&
    (revocation_count < BiasedLockingBulkRevokeThreshold) &&
    (last_bulk_revocation_time != 0) &&
    (cur_time - last_bulk_revocation_time >= BiasedLockingDecayTime)) {
    // 重置 class 中记录的偏向撤销次数为 0
    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;
    }
  • :star::star::star::star::star: jdk8u/hotspot 源码:网站阅读体验不友好,建议直接下载完整源码,在本地进行源码的阅读

预备知识

klass, lockee, LockRecord 三者的联系

lockee 是锁对象,klass 是锁对象的类元数据信息,LockRecord 是每次加锁都会产生的一个锁记录

每次加锁,无论是偏向锁、轻量级锁还是重量级锁,都会在获取锁的线程的栈空间中添加一个 LockRecord 锁记录,并让 LockRecordobj 指向 lockee

偏向锁

偏向锁以及轻量级锁的加锁流程

偏向锁的锁重入

从偏向撤销/锁升级入口 InterpreterRuntime::monitorenter 开始分析整个偏向撤销流程

什么情况下会进入 InterpreterRuntime::monitorenter 方法?

  • 获取偏向锁时/重偏向时发现锁对象已经偏向其他线程
  • 轻量级锁存在竞争
  • 轻量级锁膨胀中
  • 已经是重量级锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
// ...
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
if (UseBiasedLocking) {
// fast_enter 主要是处理偏向锁的偏向撤销,由于当前线程偏向撤销后还要继续获得锁,因此在 fast_enter 内部最后又调用了 slow_enter
// 轻量级锁存在竞争、轻量级锁膨胀中、已经是重量级锁,这三种情况也会进入 fast_enter,但会跳过所有偏向锁的偏向撤销逻辑,直接进入 fast_enter 内部 最后调用的 slow_enter
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
// slow_enter 主体是处理线程竞争锁的逻辑,主要有四种情况:
// (1) 偏向锁撤销后再次尝试获取锁
// (2) 最开始尝试获取轻量级锁
// (3) 最开始尝试获取处于膨胀中状态的锁
// (4) 最开始尝试获取重量级锁
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
// ...
IRT_END

偏向撤销逻辑主要看 ObjectSynchronizer::fast_enter 的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
if (UseBiasedLocking) {
if (!SafepointSynchronize::is_at_safepoint()) {
// 如果是 Java 线程,执行的是该分支(一般是这条分支)
// revoke_and_rebias 方法中包含了非常多种情况的处理,主要可以分为三类
// (1) 重偏向,返回 BIAS_REVOKED_AND_REBIASED
// (2) 偏向撤销,返回 BIAS_REVOKED
// (3) 锁对象不是偏向模式:可能是轻量级锁/锁膨胀/重量级锁,方法什么都不做,直接返回 NOT_BIASED
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
// 重偏向是将锁对象重新偏向当前线程,这意味着当前线程成功获取到了偏向锁
// 因此可以直接退出,不用进入最后的 slow_enter
return;
}
} else {
// 如果是 VM 线程,执行的是该分支
assert(!attempt_rebias, "can not rebias toward VM thread");
BiasedLocking::revoke_at_safepoint(obj);
}
assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
}
// 进入 `slow_enter` 存在非常多种情况:只要不是重偏向,剩下的逻辑都会进入 slow_enter,后续再进行解析
slow_enter (obj, lock, THREAD) ;
}

重点是 Java 线程那条分支,这里可能会让人比较疑惑,当前线程明明是带着偏向撤销的目的来的,怎么 revoke_and_rebias 给当前线程来了一次重偏向的操作呢?这是因为存在批量重偏向的两种情况

  • 当前线程在真正做偏向撤销操作前,还会再次检查锁对象的 epoch 是否过期。如果发现过期了(之前获取偏向锁时还未过期),说明期间有其他线程先一步做了偏向撤销,并触发了一次批量重偏向(批量重偏向操作的第一步就是将 Klassepoch 加 1),此时,直接将锁对象重偏向为当前线程即可。
  • 当前线程的这次偏向撤销操作恰好让偏向撤销计数递增到批量重偏向阈值,因此会在 VM 线程执行完批量重偏向后,再将锁对象重偏向为当前线程。

具体还是要深入看 BiasedLocking::revoke_and_rebias 的内容,主线是递增偏向撤销计数,根据计数结果去执行单次偏向撤销/批量撤销/批量重偏向:

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
BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) {
assert(!SafepointSynchronize::is_at_safepoint(), "must not be called while at safepoint");

markOop mark = obj->mark();
if (mark->is_biased_anonymously() && !attempt_rebias) {
// 奇怪的分支情况:锁对象的 markword 处于匿名可偏向状态,却又不允许重偏向
// 据说是处理偏向锁对象调用过 hashCode 的情况(这里不用关心)
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;
}
} else if (mark->has_bias_pattern()) {
// 锁对象仍然是偏向模式
Klass* k = obj->klass();
markOop prototype_header = k->prototype_header();
if (!prototype_header->has_bias_pattern()) {
// 当前线程在上次进入 InterpreterRuntime::monitorenter 到最终执行到这行代码期间
// 锁对象发生了批量撤销的情况(批量撤销的第一步就是关闭 Klass 的偏向模式),因此尝试 cas 锁对象的对象头为无锁状态
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;

} else if (prototype_header->bias_epoch() != mark->bias_epoch()) {
// 当前线程在上次进入 InterpreterRuntime::monitorenter 到最终执行到这行代码期间
// 锁对象发生了批量重偏向的情况(批量重偏向的第一步就是将 Klass 的 epoch 加 1),因此尝试 cas 锁对象的对象头使其重偏向为当前线程
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());
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;
}
}
}
}
// 递增偏向撤销计数
HeuristicsResult heuristics = update_heuristics(obj(), attempt_rebias);

if (heuristics == HR_NOT_BIASED) {
// 如果锁对象已经不是偏向状态了,就返回 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()) {
// 奇怪的分支情况:锁对象的 markword 是偏向自己的,且 epoch 也未过期
// 据说是处理偏向锁对象调用过 hashCode 的情况(这里不用关心)
// 既然是当前线程要撤销自己,那就不需要麻烦 VM 线程了,当前线程自己处理即可
ResourceMark rm;
if (TraceBiasedLocking) {
tty->print_cr("Revoking bias by walking my own stack:");
}
BiasedLocking::Condition cond = revoke_bias(obj(), false, false, (JavaThread*) THREAD);
((JavaThread*) THREAD)->set_cached_monitor_info(NULL);
assert(cond == BIAS_REVOKED, "why not?");
return cond;
} else {
// case 1
// 绝大多数情况应该是进入这个分支,当前线程要偏向撤销,且期间没有发生批量重偏向以及批量撤销等情况
// 锁对象偏向于线程 A,当前线程是线程 B,将偏向撤销操作交给 VM 线程去执行
// 下面代码最终会在 VM 线程中的 safepoint 调用 revoke_bias 方法进行偏向撤销操作
VM_RevokeBias revoke(&obj, (JavaThread*) THREAD);
VMThread::execute(&revoke);
// 一般返回的都是 BIAS_REVOKED
return revoke.status_code();
}
}

assert((heuristics == HR_BULK_REVOKE) ||
(heuristics == HR_BULK_REBIAS), "?");
// case 2
// 当前线程的这次偏向撤销,正好让 Klass 的偏向撤销计数递增到了批量重偏向的阈值或批量撤销的阈值
// 下面是执行批量重偏向或批量撤销的逻辑,最终会在 VM 线程的 safepoint 中调用 bulk_revoke_or_rebias_at_safepoint 来进行批量重偏向或批量撤销的逻辑
// 不过 bulk_revoke_or_rebias_at_safepoint 底层其实也是去调用 revoke_bias 方法!
VM_BulkRevokeBias bulk_revoke(&obj, (JavaThread*) THREAD,
(heuristics == HR_BULK_REBIAS),
attempt_rebias);
VMThread::execute(&bulk_revoke);
// 如果是 bulk_revoke 执行的是批量重偏向,返回 BIAS_REVOKED_AND_REBIASED
return bulk_revoke.status_code();
}

revoke_and_rebias 包含了非常多种情况的处理,但是前半部分其实都是对一些特殊情况的处理,比如:

  • 准备偏向撤销前再次检查 klass 是否关闭了偏向模式:如果是,说明期间由于 klass 其他锁对象发生了偏向撤销并触发了批量撤销,从而当前锁对象可以直接 cas 对象头为无锁状态(可能成功,也可能失败,但无论失败还是成功,都直接退出。失败的情况主要是当前锁对象在批量撤销时还活跃,从而在批量撤销中被直接升级为轻量级锁)。
  • 准备偏向撤销前再次检查 epoch 是否过期:如果是,说明期间由于 klass 其他锁对象发生了偏向撤销并触发了批量重偏向,使得当前锁对象的 epoch 已过期,从而可以直接进行重偏向
    这里我们重点关注其中的后半部分 case 1case 2case 1 表示执行单次撤销的情况,case 2 表示批量撤销或批量重偏向的情况。

先来看看 case 1

1
2
3
4
5
// case 1
// 创建了一个 VM_RevokeBias 类的实例,名为 revoke,后面是传入的构造函数的参数
VM_RevokeBias revoke(&obj, (JavaThread*) THREAD);
// 将 revoke 对象交给 VMThread 去执行
VMThread::execute(&revoke);

HotSpot 的内部实现中,有一系列的 VM 操作类,它们都继承自基类 VM_OperationVM_RevokeBias 就是其中一个,它重写了基类的 doit() 方法,当调用 VMThread::execute(&revoke) 时,VM 线程就会调用 revokedoit() 方法来做具体操作,其中 doit() 方法内部就调用了 revoke_bias() 方法。

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
class VM_RevokeBias : public VM_Operation {
protected:
Handle* _obj;
GrowableArray<Handle>* _objs;
JavaThread* _requesting_thread;
BiasedLocking::Condition _status_code;
traceid _biased_locker_id;

public:
VM_RevokeBias(Handle* obj, JavaThread* requesting_thread)
: _obj(obj)
, _objs(NULL)
, _requesting_thread(requesting_thread)
, _status_code(BiasedLocking::NOT_BIASED)
, _biased_locker_id(0) {}

// ...

virtual void doit() {
if (_obj != NULL) {
if (TraceBiasedLocking) {
tty->print_cr("Revoking bias with potentially per-thread safepoint:");
}

JavaThread* biased_locker = NULL;
// 调用 revoke_bias,allow_rebias 参数是 false
_status_code = revoke_bias((*_obj)(), false, false, _requesting_thread, &biased_locker);
#if INCLUDE_JFR
if (biased_locker != NULL) {
_biased_locker_id = JFR_THREAD_ID(biased_locker);
}
#endif // INCLUDE_JFR

clean_up_cached_monitor_info();
return;
} else {
if (TraceBiasedLocking) {
tty->print_cr("Revoking bias with global safepoint:");
}
BiasedLocking::revoke_at_safepoint(_objs);
}
}

接下来重点分析一下 revoke_bias 方法。
revoke_bias 方法一般是 VM 线程来执行,该方法虽然名字上看是叫撤销偏向,但却接收一个 allow_rebias 参数:

  • 该参数绝大多数情况下都是 false,比如上面的 case 1VMThread::execute(&revoke) 去做单次撤销,以及后面的 case 2 中的 VMThread::execute(&bulk_revoke) 去做批量撤销。allow_rebias == false 的情况下 revoke_bias 方法必然是去执行偏向撤销的操作。
  • 一般为 true 只存在一种情况,与 case 2 中的 VMThread::execute(&bulk_revoke) 去做批量重偏向有关,后面会提到。
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
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()) {
// 如果锁对象已经是非偏向状态,直接返回 NOT_BIASED(不是重点)
return BiasedLocking::NOT_BIASED;
}

uint age = mark->age();
// 构建两个 markword,一个是匿名偏向模式(101),一个是无锁模式(001)
markOop biased_prototype = markOopDesc::biased_locking_prototype()->set_age(age);
markOop unbiased_prototype = markOopDesc::prototype()->set_age(age);

// ...

// 获取当前锁对象偏向的线程
JavaThread* biased_thread = mark->biased_locker();
// ...

// 判断锁对象偏向的线程是否还存活
bool thread_is_alive = false;
// 如果请求的线程就是偏向线程(当前线程是 VM 线程,请求的线程指的是希望执行偏向撤销的某个 Java 线程)
if (requesting_thread == biased_thread) {
thread_is_alive = true;
} else {
// 遍历当前 jvm 的所有 java 线程,如果能找到,则说明偏向的线程还存活
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) {
// 批量重偏向的情况:如果允许重偏向,那么就将锁对象的 markword 恢复为匿名偏向状态
// 注意,这里只是恢复到匿名偏向,还没有重偏向到某个具体线程
obj->set_mark(biased_prototype);
} else {
// 单次偏向撤销 或 批量撤销的情况:否则将所对象 mark word 设置为无锁状态
obj->set_mark(unbiased_prototype);
}
// ...
return BiasedLocking::BIAS_REVOKED;
}

// 如果偏向的线程还存活,则到偏向的线程的线程栈中遍历所有的 LockRecord
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);
// 如果能找到对应的 LockRecord 说明偏向的线程还在执行同步代码块中的代码
if (mon_info->owner() == obj) {
// ...
// 此时需要直接升级为轻量级锁,直接修改偏向线程栈中的相关的 LockRecord。
// 为了处理锁重入的情况,显式将每个 LockRecord 的 displaced markword 都先置为 null
// 偏向线程栈中第一个相关的 LockRecord 会最后被遍历到,然后在后面还会再处理
markOop mark = markOopDesc::encode((BasicLock*) NULL);
highest_lock = mon_info->lock();
highest_lock->set_displaced_header(mark);
} else {
// ...
}
}
if (highest_lock != NULL) {
// 对于第一个相关的 LockRecord,需要将其 displaced markword 置为无锁状态
// 同时将第一个相关的 LockRecord 的地址设置到锁对象的 markword,自此偏向锁就完成了到轻量级锁的升级
highest_lock->set_displaced_header(unbiased_prototype);
obj->release_set_mark(markOopDesc::encode(highest_lock));
// ...
} else {
// 走到这里说明偏向线程虽然存活,但是并不在同步块中
// ...
if (allow_rebias) {
// 批量重偏向的情况:如果允许重偏向,那么就将锁对象的 markword 恢复为匿名偏向状态
// 注意,这里只是恢复到匿名偏向,还没有重偏向到某个具体线程
obj->set_mark(biased_prototype);
} else {
// 单次偏向撤销 或 批量撤销的情况:否则将所对象 mark word 设置为无锁状态
obj->set_mark(unbiased_prototype);
}
}

return BiasedLocking::BIAS_REVOKED;
}

对于 case 1 的场景而言,revoke_bias 只存在单次偏向撤销的情况:

  • 如果锁对象偏向的线程 A 已经不存活,那么就直接将锁对象设置为无锁状态
  • 如果锁对象偏向的线程 A 还存活,但是并不活跃(正在持有锁执行同步代码块),也是直接将锁对象设置为无锁状态
  • 如果锁对象偏向的线程 A 存活且活跃,锁对象将直接升级为轻量级锁(这里处理时,考虑了锁重入的情况)

我们再回过头来看 case 2 的情况,也就是批量撤销或批量重偏向的情况:

1
2
3
4
5
6
7
// case 2
// 创建了一个 VM_BulkRevokeBias 类的实例,命名为 bulk_revoke,后面是传入的构造函数的参数
VM_BulkRevokeBias bulk_revoke(&obj, (JavaThread*) THREAD,
(heuristics == HR_BULK_REBIAS),
attempt_rebias);
// 将 bulk_revoke 对象交给 VMThread 去执行
VMThread::execute(&bulk_revoke);

revoke_and_rebias 方法中执行了一次 update_heuristics 来递增偏向撤销的计数,该该方法的返回值很关键:

1
HeuristicsResult heuristics = update_heuristics(obj(), attempt_rebias);

如果偏向撤销计数达到了批量撤销的阈值,那么返回值就是 HR_BULK_REVOKE
如果偏向撤销计数达到的是批量重偏向的阈值,那么返回值就是 HR_BULK_REBIAS

在构造 VM_BulkRevokeBias 对象实例时,传入的第三个参数 (heuristics == HR_BULK_REBIAS) 就能够根据 update_heuristics 返回值的不同,最终让 VM 线程去执行批量撤销或者批量重偏向。

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
// VM_BulkRevokeBias 继承了 VM_RevokeBias,从而也是 VM_Operation 的子类
class VM_BulkRevokeBias : public VM_RevokeBias {
private:
bool _bulk_rebias;
bool _attempt_rebias_of_object;

public:
VM_BulkRevokeBias(Handle* obj, JavaThread* requesting_thread,
bool bulk_rebias,
bool attempt_rebias_of_object)
: VM_RevokeBias(obj, requesting_thread)
, _bulk_rebias(bulk_rebias)
, _attempt_rebias_of_object(attempt_rebias_of_object) {}

virtual VMOp_Type type() const { return VMOp_BulkRevokeBias; }
virtual bool doit_prologue() { return true; }

virtual void doit() {
// bulk_revoke_or_rebias_at_safepoint 方法被调用
// _bulk_rebias 为 true 表示执行批量重偏向,为 false 表示执行批量撤销
// _attempt_rebias_of_object 一般为 true,表示允许重偏向
_status_code = bulk_revoke_or_rebias_at_safepoint((*_obj)(), _bulk_rebias, _attempt_rebias_of_object, _requesting_thread);
clean_up_cached_monitor_info();
}
};

接下来重点分析一下 bulk_revoke_or_rebias_at_safepoint 方法。
bulk_revoke_or_rebias_at_safepoint 在命名上就已经反映了,该方法一般是 VM 线程来执行。

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
static BiasedLocking::Condition bulk_revoke_or_rebias_at_safepoint(oop o,
bool bulk_rebias,
bool attempt_rebias_of_object,
JavaThread* requesting_thread) {
// ...
jlong cur_time = os::javaTimeMillis();
o->klass()->set_last_biased_lock_bulk_revocation_time(cur_time);


Klass* k_o = o->klass();
Klass* klass = k_o;

if (bulk_rebias) {
// 批量重偏向的逻辑
if (klass->prototype_header()->has_bias_pattern()) {
// 自增 Klass 中的 epoch
int prev_epoch = klass->prototype_header()->bias_epoch();
klass->set_prototype_header(klass->prototype_header()->incr_bias_epoch());
int cur_epoch = klass->prototype_header()->bias_epoch();

// 遍历 JVM 所有线程栈,找出所有目前正在“活跃”的类型为 Klass 的偏向锁对象,让这些对象的 epoch 与 Klass 的 epoch 保持一致
// 这里的“活跃”我打了双引号,因为有一部分偏向锁对象可能其实并没有处在锁定状态
for (JavaThread* thr = Threads::first(); thr != NULL; thr = thr->next()) {
GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(thr);
for (int i = 0; i < cached_monitor_info->length(); i++) {
MonitorInfo* mon_info = cached_monitor_info->at(i);
oop owner = mon_info->owner();
markOop mark = owner->mark();
if ((owner->klass() == k_o) && mark->has_bias_pattern()) {
// We might have encountered this object already in the case of recursive locking
assert(mark->bias_epoch() == prev_epoch || mark->bias_epoch() == cur_epoch, "error in bias epoch adjustment");
owner->set_mark(mark->set_bias_epoch(cur_epoch));
}
}
}
}

// 虽然这里是执行批量重偏向,但是对于触发了批量重偏向的那个偏向锁对象,还会做一些处理:
// 这里就是之前提到的,调用 revoke_bias 时 attempt_rebias 参数唯一为 true 的情况
// 如果偏向锁对象真的处在锁定状态,那么这里 revoke_bias 会直接将偏向锁升级为轻量级锁
// 如果偏向锁对象所偏向的线程已死亡或者未在执行同步代码块,那么这里 revoke_bias 会将偏向锁对象会重置为匿名偏向状态,而不是直接重偏向到请求的线程,可以说这里是只做了一半的重偏向,真正重偏向到请求线程是在本方法末尾
revoke_bias(o, attempt_rebias_of_object && klass->prototype_header()->has_bias_pattern(), true, requesting_thread);
} else {
// 批量撤销的逻辑
// ...
// 将 Klass 中的偏向标记关闭,markOopDesc::prototype() 返回的是一个关闭偏向模式的 prototype
klass->set_prototype_header(markOopDesc::prototype());

// 遍历 JVM 所有线程栈,找出所有目前正在“活跃”的类型为 Klass 的偏向锁对象,逐个执行偏向撤销
// 这里的“活跃”我打了双引号,因为有一部分偏向锁对象可能其实并没有处在锁定状态
for (JavaThread* thr = Threads::first(); thr != NULL; thr = thr->next()) {
GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(thr);
for (int i = 0; i < cached_monitor_info->length(); i++) {
MonitorInfo* mon_info = cached_monitor_info->at(i);
oop owner = mon_info->owner();
markOop mark = owner->mark();
if ((owner->klass() == k_o) && mark->has_bias_pattern()) {
// 逐个执行偏向撤销,如果偏向锁对象所偏向的线程已死亡或者未在执行同步代码块,则撤销为无锁状态;否则在执行同步代码块,直接升级为轻量级锁
revoke_bias(owner, false, true, requesting_thread);
}
}
}

// 这一步其实是冗余的,在刚刚上面的双层 for 循环内部就处理过了:revoke_bias(owner, false, true, requesting_thread)
// 双层 for 循环中 owner == o 的情况是存在的
revoke_bias(o, false, true, requesting_thread);
}

// ...

BiasedLocking::Condition status_code = BiasedLocking::BIAS_REVOKED;

if (attempt_rebias_of_object &&
o->mark()->has_bias_pattern() &&
klass->prototype_header()->has_bias_pattern()) {
// 如果上面执行的是批量重偏向,有可能进入该分支
// 批量重偏向在双层 for 循环后做了一个 revoke_rebias,当触发批量重偏向的偏向锁对象不活跃时,会将其置为匿名重偏向,而没有重偏向到请求线程,这里就是真正重偏向到请求线程

// 构造一个偏向请求线程的 markword
markOop new_mark = markOopDesc::encode(requesting_thread, o->mark()->age(),
klass->prototype_header()->bias_epoch());
// 更新当前锁对象的 markword
o->set_mark(new_mark);

// 这种情况下,也会返回 BIAS_REVOKED_AND_REBIASED
// 回顾一下就是,线程 B 希望做偏向撤销操作,结果恰好这次就触发了批量重偏向
// 对于触发批量重偏向的请求线程 B,会尝试为其做一次重偏向,如果偏向锁未处于锁定状态,就会成功重偏向
// 这样,线程 B 就获取到了偏向锁,后续回到前面 fast_enter 中就可以直接退出,而不是进入 slow_enter 去尝试获取轻量级锁
status_code = BiasedLocking::BIAS_REVOKED_AND_REBIASED;
// ...
}

// ...

return status_code;
}

上面的批量重偏向以及批量撤销操作中,都涉及到了一个遍历线程栈,所有目前正在“活跃”的类型为 Klass 的偏向锁对象的操作,为什么我这里的“活跃”打了个引号呢?

先下定义:真正的活跃状态指的是,偏向锁对象目前处在锁定状态,也即所偏向的线程正在执行同步代码块的状态。

但其实存在一些偏向锁对象,它并没有在执行同步代码块,却也被批量重偏向和批量撤销操作处理了。

举个简单的例子,线程 B 去访问偏向锁,发现偏向的是线程 A,即使线程 A 此时没有在执行相应的同步代码块,即在线程 A 的线程栈中找不到关联偏向锁的 LockRecord,但由于只要从偏向锁的入口进入就会在相应的线程栈中创建一个 LockRecord 并与偏向锁关联,所以线程 B 的栈中会存在一个关联偏向锁的 LockRecord,导致偏向锁明明没有被锁定却会被处理!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CASE(_monitorenter): {
// lockee 就是锁对象
oop lockee = STACK_OBJECT(-1);
CHECK_NULL(lockee);
// 在当前线程栈找到一个空闲的 Lock Record
BasicObjectLock* limit = istate->monitor_base();
BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
BasicObjectLock* entry = NULL;
while (most_recent != limit ) {
if (most_recent->obj() == NULL) entry = most_recent;
else if (most_recent->obj() == lockee) break;
most_recent++;
}

if (entry != NULL) {
// 找到以后,将 LockRecord 的 obj 指针指向锁对象,完成关联
entry->set_obj(lockee);
// ...
}
// ...
}

总结一下,批量重偏向以及批量撤销可能“误伤”了一些不在锁定状态的偏向锁对象,这些偏向锁对象虽然有相应的 LockRecord,但实际上并没有被所偏向的线程锁定:

  • 批量重偏向的误伤:未锁定的锁对象也更新 epoch,而不是让它过期
  • 批量撤销的误伤:未锁定的锁对象被撤销偏向,被修改为无锁状态

总的来说,这种“误伤”也无伤大雅。

想要避免“误伤”的话,其实源码里面略作改动即可,也即遍历所有线程栈,如果找到一个 LockRecordobj 指向了一个偏向锁对象(对象为 Klass 类型)后,额外再到偏向锁对象所偏向的线程的栈中再检查是否存在关联的 LockRecord。但这样的话,就变成了三层循环,可能导致性能的下降。

至此,我们总算是将偏向锁撤销的完整流程走通了。

为什么偏向撤销一般是需要在 safepoint 期间来完成的?

假设偏向锁对象正在被线程 A 使用,线程 B 此时也来获取锁,在线程 B 的视角下,它知道目前锁对象已经偏向了线程 A,需要进行偏向撤销操作。

  • 偏向撤销操作需要根据锁对象是否活跃来做不同的处理。
    然而线程 B 根本无法确定锁对象是否活跃。要确定锁对象是否活跃,线程 B 必须要访问线程 A 的栈空间看看有没有对应的 LockRecord,这是不可能的,JVM 中每个线程的栈空间都是线程私有的。
  • 就算线程 B 开了天眼知道锁对象是活跃的,即线程 A 正在执行同步代码块,线程 B 也无法执行活跃情况下的偏向撤销处理。
    偏向锁活跃,此时要做偏向撤销是升级为轻量级锁:锁对象的 markword 需要设置为指向线程 A 中的第一个 LockRecord 的地址。但是线程 B 不可能访问线程 A 的栈空间,也就无法得到线程 A 中的第一个 LockRecord 的地址。

于是线程 B 只能偏向撤销的操作交给 VM 线程来完成。

到了 safepoint 期间,所有 Java 线程都停止工作,VM 线程开始活动。
VM 线程可以访问所有 Java 线程,自然就可以到线程 A 中去检查栈中是否有指向该偏向锁对象的 LockRecord,如果有就说明偏向锁活跃,线程 A 还在执行同步代码块,否则就是不活跃,线程 A 已经释放了偏向锁。根据活跃状态的不同,VM 线程就可以去做不同的处理了:

  • 偏向锁对象活跃,VM 线程此时找到线程 A 的栈中最早的那个 LockRecord,将其 displaced markword 设置为无锁状态,将其他的 LockRecorddisplaced markword 设置为 NULL,然后将该 LockRecord 的地址设置到偏向锁对象的 markword 中去,从而直接完成偏向锁到轻量级锁的升级。注意,这里的处理是兼顾了锁重入的情况。
  • 偏向锁对象不活跃,或者偏向线程已死的情况,直接将偏向锁对象的 markword 设置为无锁状态,从而完成偏向锁到无锁状态的转换。

批量重偏向

每次执行偏向撤销,都会将偏向锁对象所属的 Klass 类信息中的 revocation_count 加一。

一旦 revocation_count 到达 BiasedLockingBulkRebiasThreshold 批量重偏向阈值(一般是 20),就执行批量重偏向操作。

批量重偏向这个命名具有一定的迷惑性,初学者容易误以为,批量重偏向是立即完成的。
其实稍微想一下,就知道批量重偏向是不可能立即完成的,我们无法预知未来!

从批量重偏向的实际操作逻辑来看,其实真正将某个偏向锁的 markword 中的 threadId 偏向于新线程,是要等到下次线程来获取偏向锁时才发生的,因此批量重偏向,可以理解为是批量打上可重偏向标记。

Klass 可能有非常多的对象实例,在批量重偏向后,那些在批量重偏向时不活跃的偏向锁对象,就被打上一个可重偏向标记。
如果一个偏向锁对象带有该标记,就说明该对象目前一定是没有线程在使用的,即没有线程正在执行相应的同步代码块。这样下次有线程来获取该对象时,看到有可重偏向标记,就可以放心的执行 cas 操作,将自己的线程 id 设置到该对象的 markword 中来完成重偏向,无需担心是否会影响到其他线程。这里也可能是重偏向给自己,但无论是重偏向给谁,重偏向发生过后,被打上的可重偏向标记也随着 cas 操作被清除掉了。
至于那些批量重偏向时还活跃的对象,由于所偏向的线程还在使用它们,因此它们本就不具备可重偏向的条件,自然就不会被打上可重偏向的标记,后续如果其他线程来访问,应该将进入偏向撤销的逻辑。

“批量重偏向是为那些批量重偏向时不活跃的偏向锁对象,打上一个可重偏向标记”,这其实一种便于理解的说法。实际上,批量重偏向主要做了两件事:

  • 先将 Klass 类信息中的 epoch 加一。
  • 然后找出 Klass 所有目前正在“活跃”的偏向锁对象,让其 epochKlass 类信息的 epoch 保持一致。

可以看到,其实批量重偏向压根就没有去操作那些不“活跃”的偏向锁对象,而是反过来去操作那些“活跃”的偏向锁对象。但正因为没有操作,所有这些不活跃对象的 epoch 肯定是过期的,与 Klass 类信息中的 epoch 不一致的。这种不一致,其实是就相当于是一种可重偏向标记。

当下次有线程来获取该对象的偏向锁时,会先检查 epoch 是否过期,如果已过期,则认为是打上了可重偏向标记的不活跃对象,直接进行 cas 操作更新 epochthreadId

批量撤销

针对于某个 Klass 的批量重偏向发生后,系统就会记录这个批量重偏向的时间,如果这个 Klass 的对象实例,在接下来的指定时间内 BiasedLockingDecayTime(一般是 25 秒),发生了多次偏向撤销操作,导致 revocation_count 超过了批量撤销的阈值 BiasedLockingBulkRevokeThreshold(一般是 40),此时就会进行批量撤销。

一般来说,revocation_count 在批量重偏向后为 20,如果在批量重偏向后的 25 秒内,到达了 40,就触发批量撤销操作;否则就重置 revocation_count 为 0,从而为下次批量重偏向提供可能。

批量撤销主要做了两件件事:

  • 标记 Klass 为不可偏向,具体是修改 Klass 中的 prototype_header 为不可偏向模式。
  • 找出 Klass 所有目前正在“活跃”的偏向锁对象,逐个执行偏向撤销操作
    • 真正活跃的处于锁定状态的,会升级为轻量级锁
    • 实际不活跃的未处于锁定状态的,会修改为无锁

批量撤销后,如果创建了 Klass 的新对象实例,那么这些新对象实例创建出来就是不可偏向的无锁状态,即 markword 的后三位是 001,后续线程来加锁时是直接加轻量级锁,而不会进入加偏向锁的逻辑。

而对于那些批量撤销前,已经创建出来的 Klass 的旧对象实例:

  • 如果对象实例已经是无锁/轻量级锁/重量级锁状态,根本不会受到批量撤销的影响。
    批量撤销本质是对处于偏向状态的那些对象,逐个执行单次偏向撤销操作!
  • 如果对象实例是偏向锁状态,此时具体可分为两类:
    • 批量撤销时正“活跃”的对象,遍历所有 Java 线程栈空间能找到对应 LockRecord 的:如上面所说,真正活跃的会升级为轻量级锁,实际不活跃但被误以为活跃的,会修改为无锁
    • 确实不活跃的对象,遍历所有 Java 线程栈空间都找不到对应的 LockRecord 的,这一类对象在重新活跃时,可能受到影响:
      • epoch 没过期且偏向的是当前线程,则不受影响,后续直接进入同步代码块,锁对象依然是偏向锁
      • 其他情况,包括 epoch 过期,匿名偏向状态,或者不是偏向当前线程的,会直接 cas 关闭锁对象的偏向模式,置为无锁状态

题外话:批量撤销时,为什么不干脆再把那些确实不活跃的偏向锁对象也一并处理为无锁呢?

个人认为,可能是处于以下的考虑:

  • 主要原因是,“活跃”的偏向锁对象容易定位到。
    每个“活跃”的偏向锁对象必然在某个 Java 线程的栈中存在一个 LockRecord 与之关联,遍历 JVM 所有线程栈中的 LockRecord,如果某个 LockRecordobj 字段指向了给定 Klass 类型的对象,那么这个对象就是“活跃”的偏向锁对象。
    而确实不活跃的偏向锁对象就麻烦了,无法通过 LockReocrd 机制来指引,要找到这些类型为 Klass 的对象实例可能需要遍历整个堆,这无疑是非常耗时的,会极大延长批量撤销时的 safepoint 时间!

  • 确实不活跃的偏向锁对象可能非常多,且可能以后都不会使用了,在宝贵的 safepoint 时间下立即对它们处理性价比不高。
    要知道,在开启偏向锁的情况下,每个 Java 对象被创建时都默认是匿名偏向状态,可见不活跃的偏向锁对象一定是很多的!但其中绝大多数可能以后都不会使用了,在宝贵的 safepoint 时间下立即对它们处理性价比不高,让这些不活跃对象后续重新活跃时再处理,是种高效且自然的做法。

  • 确实不活跃的偏向锁对象虽然在批量撤销中没有直接处理,但它们都隐式地带上了一个不活跃的标记,这个标记的存在保证了不活跃对象后续重新活跃时,再进行偏向撤销操作会非常容易。
    一般的偏向撤销操作是需要等到 safepoint 去执行的。如果能确定锁对象是不活跃的,那么偏向撤销操作其实根本就无需等到 safepoint,直接 cas 将锁对象的 markword 设为无锁状态即可。
    恰好,批量撤销后,所有活跃的偏向锁对象都在批量撤销期间已被处理,JVM 中剩余的 Klass 偏向锁对象都必然是不活跃的!这就是所谓的隐式地带上了一个不活跃的标记。
    既然不活跃对象在非 safepoint 的情况下也能很方便处理,那何必浪费宝贵的 safepoint 时间呢,而且重新活跃再处理,不活跃就一直不处理,显然是更高效的做法。

出于类似的考虑,批量重偏向中也是处理那些活跃的偏向锁对象。


题外话:批量撤销可不可以只将 Klass 标记为不可偏向就结束,而不去逐个撤销那些活跃的偏向锁对象呢?这样批量撤销时的 safepoint 就更短了。

如果不去逐个撤销那些活跃的偏向锁对象,那么批量撤销后 JVM 中剩余的 Klass 偏向锁对象有可能是活跃的,也有可能是不活跃的,此时任意一个对象后续需要偏向撤销时,由于不确定它是否是活跃的,就需要等到 safepoint 才能去完成偏向撤销操作。

因此,批量撤销所在的 safepoint 时间可能是短了,但是省下来的时间其实是被转移到了后续多个 safepoint 中了。与其让将这些偏向锁对象放到后续的往往是多个 safepoint 来分批做偏向撤销,倒不如直接就在这次 safepoint 期间就批量进行偏向撤销。

轻量级锁

轻量级锁的锁重入

为什么 Hotspot 选择在线程栈中添加多个 LockRecord 来表示锁重入?

一个简单的方案是将锁重入次数记录在对象头的mark word中,但mark word的大小是有限的,已经存放不下该信息了。
另一个方案是只创建一个Lock Record并在其中记录重入次数,Hotspot没有这样做的原因我猜是考虑到效率有影响:每次重入获得锁都需要遍历该线程的栈找到对应的Lock Record,然后修改它的值。

目前 Hotspot 的用多个 LockRecord 表示锁重入的做法,每次获取锁和释放锁,一样是需要遍历线程的栈去找对应的 LockRecord

个人猜测,可能和效率关系不大,实现方便应该是主要因素:特别是要考虑轻量级锁升级到重量级锁的逻辑。

ObjectSynchronizer::slow_enter 补充分析

前面分析偏向锁撤销流程时,我们简单分析过 fast_enterslow_enter,但重点是放在 fast_enter 上。现在我们重点看 slow_enter

进入 slow_enter 存在非常多种情况,我们关注那些具有代表性的,能串通锁升级流程的情况:

  • 锁一开始是偏向锁,偏向于线程 A,当前线程 B 获取偏向锁失败,执行偏向撤销
    • 线程 B 执行偏向撤销时,线程 A 已不存活或未在同步代码块内,线程 B 偏向撤销将锁变为无锁状态,然后进入 slow_enter
    • 线程 B 执行偏向撤销时,线程 A 在同步代码块内,线程 B 偏向撤销将锁变为轻量级状态,然后进入 slow_enter
  • 锁一开始是轻量级锁状态,被线程 A 持有,当前线程 B 尝试以轻量级锁方式获取锁,失败,跳过偏向撤销流程,进入 slow_enter
  • 锁一开始是轻量级锁状态,被线程 A 持有,线程 C 来获取轻量级锁失败,跳过偏向撤销流程,进入 slow_enter 执行锁膨胀。当前线程 B 尝试以轻量级锁方式获取锁,由于锁处于膨胀中,故失败,跳过偏向撤销流程,进入 slow_enter
  • 锁一开始是重量级锁状态,被线程 A 持有,当前线程 B 尝试以轻量级锁方式获取锁,由于锁已经膨胀为重量级锁,故失败,跳过偏向撤销流程,进入 slow_enter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
if (UseBiasedLocking) {
if (!SafepointSynchronize::is_at_safepoint()) {
// 如果是 Java 线程,执行的是该分支(一般是这条分支)
// revoke_and_rebias 方法中包含了非常多种情况的处理,主要可以分为三类
// (1) 重偏向,返回 BIAS_REVOKED_AND_REBIASED
// (2) 偏向撤销,返回 BIAS_REVOKED
// (3) 锁对象不是偏向模式:可能是轻量级锁/锁膨胀/重量级锁,方法什么都不做,直接返回 NOT_BIASED
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
// 重偏向是将锁对象重新偏向当前线程,这意味着当前线程成功获取到了偏向锁
// 因此可以直接退出,不用进入最后的 slow_enter
return;
}
} else {
// 如果是 VM 线程,执行的是该分支
assert(!attempt_rebias, "can not rebias toward VM thread");
BiasedLocking::revoke_at_safepoint(obj);
}
assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
}
// 进入 `slow_enter` 存在非常多种情况:只要不是重偏向,剩下的逻辑都会进入 slow_enter
slow_enter (obj, lock, THREAD) ;
}

现在我们来分析一下 slow_enter 方法:

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
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
markOop mark = obj->mark();
assert(!mark->has_bias_pattern(), "should not see bias pattern here");

if (mark->is_neutral()) {
// code 1:对象是无锁状态,升级为轻量级锁后就退出
lock->set_displaced_header(mark);
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
TEVENT (slow_enter: release stacklock) ;
return ;
}
} else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
assert(lock != mark->locker(), "must not re-lock the same lock");
assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
// code 2 奇怪的分支:对象是轻量级锁,且锁的持有者是当前线程,锁重入后就退出
lock->set_displaced_header(NULL);
return;
}


// ...

// 前面所有处理都是再次检查,是否实际上并不存在多个线程的竞争,如果不存在就直接处理并返回,避免走到下面
// code 3:走到这里说明已经是存在多个线程竞争锁了,需要膨胀为重量级锁,然后当前线程再去获取重量级锁
lock->set_displaced_header(markOopDesc::unused_mark());
ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}

slow_enter 很简短,主要逻辑是:

  • 先最后再检查一下,是否实际上不存在多线程竞争的情况,是的话,直接处理并返回,避免锁膨胀为重量级锁
    比如 code 1 处的无锁情况,典型情况是:一开始是偏向锁,偏向于线程 A,当前线程 B 获取偏向锁失败,执行偏向撤销操作时,线程 A 已不存活或未在同步代码块内,线程 B 偏向撤销将锁变为无锁状态,线程 B 走到 code 1 去获取锁时实际上不存在竞争情况
  • 前面没有返回的话,说明锁存在多线程竞争,此时将当前线程最开始申请的 LockRecordheader 标记为 unused,表示该 LockRecord 对应的加锁情况是,锁膨胀后拿到重量级锁 Monitor 去加锁的。这个 unused 标记在解锁时将会被用到,用以和其他情况做区分。

:question: slow_enterelse if 分支,也即 code 2 处是处理实际上不存在多线程竞争的一种情况,轻量级锁的锁重入的情况,但我实在是想不到哪些情况能进入该分支……只能猜测或许是一些非常规情况(如在某些不恰当的时机调用了 Object#hashCode 方法)。

并发程序的运行情况往往很复杂,会有很多意想不到的情况,想要完全考虑周全是很困难的,另外验证并发程序的正确性也是一件麻烦事。
因此从并发编程实践的角度来说,就算诸如上面的 else if 分支处理是多余的,那无非也就多了一些冗余的代码,换来的好处却是显而易见的:

  • 首先就是能减少程序员并发编程时的很多心智负担,一来不用去考虑一些边边角角的情况,二来能对并发程序进行划分,为下一阶段的并发程序提供一个隐式的断言。
  • 另外如果此类的分支真的命中,可以提高并发程序的性能或安全性。

锁释放失败入口 InterpreterRuntime::monitorexit 分析

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
CASE(_monitorexit): {
oop lockee = STACK_OBJECT(-1);
CHECK_NULL(lockee);
// derefing's lockee ought to provoke implicit null check
// find our monitor slot
BasicObjectLock* limit = istate->monitor_base();
BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
// 从低往高遍历栈的 Lock Record
while (most_recent != limit ) {
// 如果 Lock Record 关联的是该锁对象
if ((most_recent)->obj() == lockee) {
BasicLock* lock = most_recent->lock();
markOop header = lock->displaced_header();
// 开始锁释放流程

// 首先将 LockRecord 的 obj 指向 NULL
most_recent->set_obj(NULL);
// 如果是偏向模式,仅仅 LockRecord 的 obj 指向 NULL 即完成释放。因此偏向锁的释放不存在失败的情况

if (!lockee->mark()->has_bias_pattern()) {
// 否则锁不处于偏向模式,要走轻量级锁或重量级锁的释放流程
// 走到这里 LockRecord 存在三种情况:
// (1) header 为 NULL,该 LockRecord 对应的加锁情况是:轻量级锁重入
// 目前锁可能为轻量级锁,也可能是膨胀中,也可能为重量级锁
// (2) header 为 lockee 的 markword 的无锁状态,该 LockRecord 对应的加锁情况是:轻量级锁初次锁定
// 目前锁可能为轻量级锁,也可能是膨胀中,也可能为重量级锁
// (3) header 为 unused 状态,该 LockRecord 对应的加锁情况是:锁膨胀后的所有加锁情况
// 目前锁只可能为重量级锁
bool call_vm = UseHeavyMonitors;
// header 为 NULL 对应轻量级锁重入的加锁情况,可以直接结束,无论目前锁状态是什么。
if (header != NULL || call_vm) {
// 如果 header 不为 NULL,做 CAS 解锁,只有一种加锁情况可以 CAS 成功解锁:轻量级锁初次锁定 且 锁目前仍然为轻量级锁的情况
if (call_vm || Atomic::cmpxchg_ptr(header, lockee->mark_addr(), lock) != lock) {
// 其他加锁情况均会 CAS 解锁失败:
// (1) 轻量级锁初次锁定,但 锁目前处于膨胀中或已经是重量级锁状态。
// (2) 锁膨胀后的所有加锁情况,也即使用重量级锁 Monitor 加锁
// 失败后,先将 LockRecord 的 obj 还原,重新指向 lockee 锁对象,然后调用 monitorexit 方法
most_recent->set_obj(lockee);
CALL_VM(InterpreterRuntime::monitorexit(THREAD, most_recent), handle_exception);
}
}
}
UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
}
// 处理下一条Lock Record
most_recent++;
}
// Need to throw illegal monitor state exception
CALL_VM(InterpreterRuntime::throw_illegal_monitor_state_exception(THREAD), handle_exception);
ShouldNotReachHere();
}

以上是锁释放流程。可以看到,进入锁释放失败入口 InterpreterRuntime::monitorexit 只可能存在两种情况:

  • 线程 A 初次锁定轻量级锁
    • 线程 B 来获取轻量级锁导致锁膨胀,在锁膨胀中途,线程 A 想要以轻量级锁方式释放锁,失败
    • 线程 B 来获取轻量级锁导致锁膨胀,锁膨胀为重量级锁后,线程 A 想要以轻量级锁方式释放锁,失败
  • 线程 A 在锁膨胀后的所有加锁,也即使用重量级锁 Monitor 加锁,想要以轻量级锁方式释放锁,失败

InterpreterRuntime::monitorexit 的代码很简短,主要是调用了 ObjectSynchronizer::slow_exit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem))
//...

// 将当前释放锁的线程和 LockRecord 打包封装为一个 h_obj 对象
Handle h_obj(thread, elem->obj());
// ...
// 调用 slow_exit
ObjectSynchronizer::slow_exit(h_obj(), elem->lock(), thread);
// Free entry. This must be done here, since a pending exception might be installed on

// slow_exit 结束后,解锁完毕,将 LockRecord 的 obj ref 指针重新设置为 NULL,完成 LockRecord 的释放
elem->set_obj(NULL);
...
IRT_END

ObjectSynchronizer::slow_exit 调用的是 ObjectSynchronizer::fast_exitfast_exit 最后锁膨胀得到 Monitor 后再调用其 exit 方法解锁。

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
void ObjectSynchronizer::slow_exit(oop object, BasicLock* lock, TRAPS) {
fast_exit (object, lock, THREAD);
}
void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
// ...
markOop dhw = lock->displaced_header();
markOop mark ;
if (dhw == NULL) {
// 奇怪的分支 code 1: 轻量级锁重入,什么也不做,直接退出即可
// ...
return ;
}

mark = object->mark() ;

// 奇怪的分支 code 2: 轻量级锁初次锁定 且 锁目前仍然为轻量级锁的情况,CAS 解锁
if (mark == (markOop) lock) {
assert (dhw->is_neutral(), "invariant") ;
if ((markOop) Atomic::cmpxchg_ptr (dhw, object->mark_addr(), mark) == mark) {
TEVENT (fast_exit: release stacklock) ;
return;
}
}

// 前面所有处理都是再次检查,是否不需要锁膨胀就可以解锁,如果是就处理,避免走到下面
// 走到这里说明是重量级锁或者解锁时发生了竞争,膨胀后调用重量级锁 Monitor 的 exit 方法。
ObjectSynchronizer::inflate(THREAD, object)->exit (true, THREAD) ;
}

重量级锁

为什么需要一个膨胀中的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Why do we CAS a 0 into the mark-word instead of just CASing the
// mark-word from the stack-locked value directly to the new inflated state?
// Consider what happens when a thread unlocks a stack-locked object.
// It attempts to use CAS to swing the displaced header value from the
// on-stack basiclock back into the object header. Recall also that the
// header value (hashcode, etc) can reside in (a) the object header, or
// (b) a displaced header associated with the stack-lock, or (c) a displaced
// header in an objectMonitor. The inflate() routine must copy the header
// value from the basiclock on the owner's stack to the objectMonitor, all
// the while preserving the hashCode stability invariants. If the owner
// decides to release the lock while the value is 0, the unlock will fail
// and control will eventually pass from slow_exit() to inflate. The owner
// will then spin, waiting for the 0 value to disappear. Put another way,
// the 0 causes the owner to stall if the owner happens to try to
// drop the lock (restoring the header from the basiclock to the object)
// while inflation is in-progress. This protocol avoids races that might
// would otherwise permit hashCode values to change or "flicker" for an object.
// Critically, while object->mark is 0 mark->displaced_mark_helper() is stable.
// 0 serves as a "BUSY" inflate-in-progress indicator.

其实注释已经说得非常清楚了,我结合自己的理解翻译一下:

为什么我们要将膨胀中状态(即 0) CAS 到锁对象的 markword 中,而不是直接将 markword 从指向 LockRecord 的地址值 CAS 到指向重量级锁 Monitor 的地址呢?

考虑线程对轻量级锁进行最后一次解锁时的情况,它会试图使用 CAS 将 LockRecorddisplaced mark word 中存放的 lockeemarkword 的无锁状态,重新挪回到 lockee 中去,从而 lockee 恢复为无锁状态。

回忆一下,锁对象原始 markword 头部信息可能存放在哪些位置:
(a) 锁对象头部,此时处于无锁状态
(b) 栈空间中与锁对象关联的第一个 LockRecorddisplaced mark word
(c) 重量级锁 Monitorheader

JDK1.6 以前,synchronized 只有 Monitor 重量级锁,彼时如果对象已经加锁了,想要获取重量级锁对象的原始 markword 头部信息,就需要到 (c) 中去获取。
后续引入轻量级锁后,如果对象已经加轻量级锁了,想要获取轻量级锁对象的原始 markword 头部信息,就要到 (b) 中获取;当然如果对象加的是重量级锁,获取重量级锁对象的原始 markword 头部信息,依然是到 (c) 中去获取。

为了保证轻量级锁升级到重量级锁后,依旧能够正确地获取原始 markword 头部信息,锁膨胀时,就必须将原来存放在 (b) 中的原始 markword 头部信息,复制一份放到 (c) 中去,并保证这个膨胀升级过程中原始信息如 hashCode 的不变性。

如果不设置一个膨胀中的状态,就会无法保证膨胀升级过程中的原始信息不变性,考虑如下情况:

  1. 线程 A 调用了 hashCode
  2. 线程 A 进行轻量级锁的初次锁定
  3. 线程 B 尝试获取轻量级锁,开始进行锁膨胀,锁膨胀至少包含下面的操作:
    3.1 将锁对象的 markword 从指向 LockRecord 改为指向 Monitor
    3.2 将 LockRecord 中的原始头部信息,复制到 Monitorheader 中去
  4. 由于整个锁膨胀不是原子的,假设线程 B 执行完 (3.1) 后,线程 A 再次调用了 hashCode
    此时,锁对象的 markword 表示对象已经是重量级锁状态,因此线程 A 回到 Monitor 中去获取 hashCode,但线程 B 还未执行 (3.2),因此返回必然是错误值。
  5. 等到线程 B 执行完 (3.2) 以后,线程 A 再次调用 hashCode 就可以拿到正确结果了。

注释将这种 hashCode 短暂不一致的情况称为 hashCode 值的闪烁现象。即使调转 (3.1) 和 (3.2) 的顺序,闪烁现象一样存在!

注释提到的破坏不变形的情况更加糟糕!线程 B 执行了 (3.1) 但还没来得及执行 (3.2) 时,线程 A 释放了锁,此时就是重量级锁的释放流程,有可能将 Monitor 中错误的 header 放回到锁对象的 markword 中。这样释放锁后,对象的对象头部不仅 markword 信息可能是错的,还可能没有正确地被设为无锁状态,甚至这种错误信息还是长期存在的。

为了应对这种情况,锁膨胀加入了一个膨胀中的(0)状态:
3.1 将对象的 markword 从指向 LockRecord 改为膨胀中的 0 状态
3.2 将 LockRecord 中的原始头部信息,复制到 Monitorheader 中去
3.3 将对象的 markword 从膨胀中的 0 状态改为指向 Monitor
线程 B 执行了 (3.1) 但还没来得及执行 (3.2) 时,线程 A 释放了锁,此时线程 A 发现锁处于膨胀中(0)状态,就会进入自旋忙等,直到线程 B 执行了 (3.2) 乃至 (3.3) 后,线程 A 才能够真正去做重量级的锁释放。

关键在于:只要锁处于膨胀中(0)状态,那么最终要换回到对象中的原始 markword 头部信息就是稳定的。

Monitor 机制下的 wait/notify 执行过程

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
public class WaitNotifyTest {
public static void main(String[] args) {
Object lock = new Object();
lock.hashCode(); // 禁用偏向锁

new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程A等待获取lock锁");
synchronized (lock) {
try {
System.out.println("线程A获取了lock锁");
Thread.sleep(1000);
System.out.println("线程A将要运行lock.wait()方法进行等待");
lock.wait();
System.out.println("线程A等待结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "Thread-A").start();

new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程B等待获取lock锁");
synchronized (lock) {
System.out.println("线程B获取了lock锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程B将要运行lock.notify()方法进行通知");
lock.notify();
}
}
}, "Thread-B").start();
}
}

简单分析一下上述代码的执行过程:

  • 首先线程 A 获取到 lock 锁,此时是轻量级锁状态,获取到锁后线程 A 开始执行同步代码块内容
  • 线程 B 尝试获取 lock 锁,但锁已经被线程 A 占用,此时线程 B 进行锁膨胀操作,膨胀后发现锁依然被线程 A 占用,封装线程 B 为 ObjectWaiter 插入到 Monitor.cxq 队列的队首,并 park 进入阻塞状态。
  • 线程 A 执行到 lock.wait() 时,先将线程 A 封装为 ObjectWaiter 并添加到 Monitor.waitSet 中,然后进行锁释放操作,首先将 Monitor.owner 置为 NULL,然后找到 Monitor.cxq 中正在阻塞等待的线程 B 的 ObjectWaiter 从而可以唤醒线程 B,最后 park 进入阻塞状态。
  • 线程 B 被唤醒后获得锁,执行同步代码块内容
  • 线程 B 执行到 lock.notify() 时,会将 Monitor.waitSet 中存放的线程 A 的 ObjectWaiter 移动到 Monitor.entryList 中,而不会立刻唤醒
  • 线程 B 继续执行同步块中的剩余代码,执行完毕后释放锁,首先将 Monitor.owner 置为 NULL,然后找到 Monitor.entryList 中阻塞等待的线程 A 的 ObjectWaiter 从而可以唤醒线程 A
  • 线程 A 被唤醒后获得锁,执行剩余的同步代码块内容,执行完毕后释放 lock 锁

搁浅现象

当竞争发生时,选取一个线程作为 _Responsible_Responsible 线程调用的是有时间限制的 park 方法,其目的是防止出现搁浅现象。

:question: 实在想不明白搁浅现象是个什么现象,下面贴一段 ObjectMonitor::exit 方法定义前的一段注释,后续有能力再回头研究…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1-0 exit
// ~~~~~~~~
// ::exit() uses a canonical 1-1 idiom with a MEMBAR although some of
// the fast-path operators have been optimized so the common ::exit()
// operation is 1-0. See i486.ad fast_unlock(), for instance.
// The code emitted by fast_unlock() elides the usual MEMBAR. This
// greatly improves latency -- MEMBAR and CAS having considerable local
// latency on modern processors -- but at the cost of "stranding". Absent the
// MEMBAR, a thread in fast_unlock() can race a thread in the slow
// ::enter() path, resulting in the entering thread being stranding
// and a progress-liveness failure. Stranding is extremely rare.
// We use timers (timed park operations) & periodic polling to detect
// and recover from stranding. Potentially stranded threads periodically
// wake up and poll the lock. See the usage of the _Responsible variable.

重量级锁的释放 ObjectMonitor::exit 分析

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
void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {
// ...

for (;;) {
assert (THREAD == _owner, "invariant") ;
// Knob_ExitPolicy 默认为 0
if (Knob_ExitPolicy == 0) {
// code 1:当前线程先释放锁,这时如果有其他线程进入同步块则能获得锁
OrderAccess::release_store_ptr (&_owner, NULL) ; // drop the lock
OrderAccess::storeload() ; // See if we need to wake a successor

// code 2:如果当前已经没有等待的线程或已经有假定继承人,则无需做后续唤醒操作,直接退出
if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
TEVENT (Inflated exit - simple egress) ;
return ;
}
TEVENT (Inflated exit - complex egress) ;
// code 3:后续唤醒操作必须独占锁执行,因此 cas 设置 _owner 为当前线程
if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {
return ;
}
TEVENT (Exit - Reacquired) ;
}
// ...

// code 4:根据 QMode 不同(QMode 默认为 0),执行不同的唤醒策略,来选择 _cxq 或 _EntryList 中合适的线程,调用 ExitEpilog 方法进行唤醒
// ...
}
}

exit 方法中 code 1 及以前的代码必然是单线程执行的,不存在并发情况,因为锁是独占的,只有获取锁的那个线程才能执行到 exit 方法来做锁释放逻辑。

但这并不意味着,整个 exit 方法都是不存在并发的,其实 code 1 之后的代码都是要考虑并发情况的,其中:

  • code 1 释放锁之后的代码一直到 code 3 这几行代码是多个线程并发执行的情况,code 3 中的 cas 操作也说明了这一点。
  • code 4 的代码不存在多线程并发执行的情况,其中主要考虑的是 _cxq 的并发安全。因为唤醒可能会涉及到对 _cxq 队列的一些操作,但同时可能有大量的其他线程获取锁并阻塞,这些线程也需要操作 _cxq:向 _cxq 中插入自己的 ObjectWaiter

code 1 处当前线程一旦将 owner 设置为 NULL 后,锁其实就已经释放掉了,此时如果有新的线程来访问同步代码块,就立刻可以 cas 获得锁。

  • 假设最初是线程 A 来持有锁,它在 code 1 处将 ownerNULL 从而释放了锁。
  • 然后线程 B 来访问同步代码块,发现 ownerNULL,因此获得锁,获得锁以后,它很快执行完了同步代码块的内容,它也在 code 1 处将 ownerNULL 从而释放了锁。
  • 此时线程 A 和线程 B 可能同时处在 code 2 的位置想要继续往下执行,这就出现了并发

code 2 处:

  • 如果已经没有等待的线程,那么并发的线程 A 和线程 B 都直接退出,无需进行后面 code 4 的唤醒操作,这个很好理解。
  • 如果还存在等待的线程,那么就需要做后面 code 4 的唤醒操作。假设是线程 A 完成了后续的唤醒并选定了假定继承人,从而 _succ 可能就非 NULL,这时候线程 B 并发执行 code 2,发现 _succNULL 就可以直接退出。

code 3 处:

  • 如果当前已经有其他线程,比如新线程 C 获取到了锁,那么线程 A 和线程 B 将在 code 3 处 CAS 失败直接退出,无需后面的唤醒操作。这个很好理解,到时后线程 C 释放的时候,它来负责唤醒。
  • 如果当前没有其他线程获取到锁,此时为了避免唤醒多个线程,让线程 A 和线程 B 去 cas 竞争,从而保证只有一个线程可以进入后续的 code 4 去做唤醒。

为什么要保证只有一个线程进入 code 4 呢?来看一下 ObjectMonitor::ExitEpilog 的代码就懂了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) {
assert (_owner == Self, "invariant") ;

// 将从 _cxq 或 _EntryList 中选中的线程设为假定继承人 _succ
_succ = Knob_SuccEnabled ? Wakee->_thread : NULL ;
ParkEvent * Trigger = Wakee->_event ;
Wakee = NULL ;

// 释放锁,注意此时假定继承人还处在 park 状态,因此这时如果有其他线程进入同步块则能获得锁
OrderAccess::release_store_ptr (&_owner, NULL) ;
OrderAccess::fence() ;

if (SafepointSynchronize::do_call_back()) {
TEVENT (unpark before SAFEPOINT) ;
}

DTRACE_MONITOR_PROBE(contended__exit, this, object(), Self);
// 调用 unpark,线程真正被唤醒,继续执行 ObjectMonitor::EnterI 方法
Trigger->unpark() ;

// ...
}

多个线程来唤醒,就有多个线程去执行 ExitEpilog 方法,从而存在线程安全问题。
假设线程 A 和线程 B 都可以进入 code 4 执行 ExitEpilog 方法

  • 线程 A 执行 ExitEpilog 方法将 owner 设置为 NULL 释放锁,此时新线程 D 来访问同步代码块获得了锁,执行同步代码块
  • 线程 D 还未执行完同步代码块。线程 B 也执行 ExitEpilog 方法将 owner 设置为 NULL 释放锁,明明是线程 D 持有锁,可却让线程 B 去释放了,此时如果新线程 E 来访问同步代码块,也能获得锁,从而线程 D 和线程 E 都在执行同步代码块

除此 ExitEpilog 方法之外,唤醒还包括了一系列对 _cxq_EntryList 的操作,这些操作在并发执行时也可能存在线程安全问题。

轻量级锁重入后,锁膨胀并再次发生多次重量级锁重入后,是什么情况

假设线程 A 初次锁定轻量级锁,然后又发生了一次轻量级锁重入,而后线程 B 来获取轻量级锁导致锁膨胀,膨胀后的情况如下:

膨胀后,线程 A 再次获取锁,发生一次重量级锁重入,情况如下:

重量级锁重入的这次加锁对应的 LockRecord 3,其 displaced mark word 被标记为 unused

另外 Monitor 的变化是:

  • _recursions 加 1
  • owner 从指向 LockRecord 1 变为指向线程 A
  • OwnerIsThread 从 0 置为 1

后续如果继续有重量级锁重入,只需要递增 _recursions 即可,不再需要修改 ownerOwnerIsThread

何时重量级锁对象恢复到无锁状态

你可能注意到了,重量级锁对象的释放,似乎只是将 Owner 置为 null 就结束了。那何时重量级锁对象会恢复到无锁状态呢?下面来探讨这个问题

重量级锁的释放大致可以分为两种场景:

  • Monitor 中存在正在等待的其他线程,此时释放逻辑是先将 Owner 置为 null,然后一般是从 cxqEntryList 中取出一个线程并唤醒作为候选,候选竞争成功后才作为新的 Owner。注意是单一唤醒而不是群体唤醒,具体策略取决于 QMode,一般是取出 EntryList 的队首线程。
  • Monitor 中已经不存在正在等待的其他线程,此时释放逻辑是 Owner 置为 null 就直接结束重量级锁的释放逻辑。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // ObjectMonitor::exit 
    // 释放锁,将 owner 置为 NULL
    OrderAccess::release_store_ptr (&_owner, NULL);
    OrderAccess::storeload();
    // 如果 EntryList 和 cxq 中均没有等待的线程,直接 return 退出
    if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
    TEVENT (Inflated exit - simple egress);
    return;
    }

也就是说,重量级锁的释放方法 ObjectMonitor::exit 中,并没有包含对象 markword 恢复到无锁状态的逻辑。

其实对象恢复到无锁状态的逻辑,是在锁的撤销膨胀逻辑中实现的,撤销膨胀操作发生在 safepoint

safepoint 是一个所有 Java 线程都暂停执行的点,系统可以执行一些必须在所有线程都暂停时才能做的任务(比如某些 GC 任务,之前提到的偏向锁撤销等),SafepointSynchronize::do_cleanup_tasks 描述了在 safepoint 期间执行的一系列任务,其中就包括 ObjectSynchronizer::deflate_idle_monitors

ObjectSynchronizer::deflate_idle_monitors 被调用时,它会遍历所有存在的 Monitor,查找那些空闲的 monitor,并分别调用 ObjectSynchronizer::deflate_monitor 完成具体的撤销膨胀操作。

当与锁对象关联的 Monitordeflate 时,系统会将存放在 Monitorheader 中锁对象的原始的 markword 头部信息取出,重新设置到锁对象的对象头的 markword 上,从而锁对象恢复到无锁状态。相关源码如下:

1
2
3
4
5
// ObjectSynchronizer::deflate_monitor
// mid 就是 Monitor 对象,取出保存在 header 中的原始信息如 hashcode,重新设置到锁对象的对象头的 markword 中,从而锁对象恢复到无锁状态
obj->release_set_mark(mid->header());
// 清除 Monitor 对象数据,为后续归还到一个全局空闲列表做好准备
mid->clear();