synchronized是掌握Java高并发的关键知识点,底层源码更是面试重灾区。本文从源码学习synchronized的原理,讲解对象头、偏向锁、轻量级锁、重量级锁等概念。
扫码关注《Java学研大本营》
Java对象头原理
对象头就和它名字一样,描述的就是JVM层面一个Java对象的头部信息。下面请看源码。
class oopDesc { // 这个类就是所有对象头部的基类
private:
/* 标记字段,其实就是markOopDesc,只不过这里用了typedef类型定义,
源码:typedef class markOopDesc* markOop; */
volatile markOop _mark;
union _metadata { // 指向对象所属class对象指针
Klass* _klass;
narrowKlass _compressed_klass; // 开启指针压缩后,指向压缩后的class对象
} _metadata;
}
class markOopDesc: public oopDesc { // 描述了标记位,其实就是带标记的oop对象
private:
uintptr_t value() const { return (uintptr_t) this; }
public:
// 定义了一些枚举常量,注意,常量都是以位bit为单位
enum { age_bits = 4, // 对象年龄4位
lock_bits = 2, // 锁标记2位
biased_lock_bits = 1, // 偏向锁1位
// 最大hash值位数 = CPU位数 - 年龄位数 - 锁位数 - 偏向锁位数
max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits, // 最大值为31位
cms_bits = LP64_ONLY(1) NOT_LP64(0), // CMS占用位数,根据CPU位数选择1或0
epoch_bits = 2 // 用于支持偏向锁2位
}
以上两个类oopDesc和markOopDesc共同描述了一个对象头部的样子,对象数据(32位或64位),指向对象所属klass对象指针。对象数据的所有位按照功能切割成不同位数,并且markOopDesc对每个位数提供了详细描述,给出了这些位分别在所有位中的位置(通过shift偏移量)。这里我们还是通过全局描述的方式来给读者直观展示在32位机和64位机中,对象数据的不同表现,以及锁状态位的不同状态。
对象头部bit描述 (采用大端序放置):
32 bits: // 在32位平台中
--------
// 正常对象:25位hashCode,4位年龄位,偏向锁1位,2位锁标记
hash:25 ------------>| age:4 biased_lock:1 lock:2
/* 已经被偏向锁锁定的对象:23位指向JavaThread线程对象的指针,2位偏向锁epoch,4位年龄,偏向锁1位,2位锁标记 */
JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2
// CMS 释放空闲块
size:32 ------------------------------------------>|
// CMS 晋升对象:29位转移后对象指针,3位晋升标志位
PromotedObject*:29 ---------->| promo_bits:3 ----->|
64 bits:// 在64 位平台中
--------
// 正常对象:25位未使用,1位未使用,31 位hashCode,4位年龄位,偏向锁1位,2位锁标记
unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2
/* 已经被偏向锁锁定的对象:54位指向JavaThread线程对象的指针,2位偏向锁epoch,4位年龄,偏向锁1位,2位锁标记 */
JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2
// CMS 晋升对象:61位对象指针,3位晋升标志位
PromotedObject*:61 --------------------->| promo_bits:3 ----->|
// CMS 释放空闲块
size:64 ----------------------------------------------------->|
// CMS状态下的普通对象
unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2
// CMS状态下的偏向锁对象
JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2
// CMS状态下的晋升对象
narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->|
// CMS状态下的空闲对象
unused:21 size:35 -->| cms_free:1 unused:7 ------------------>|
// - hash包含唯一值且最大值为31位,在64位机中hash值也不会超过32位,因为它们不适合生成一个掩码。
// - 偏向锁模式被使用于偏向一个线程,当偏向锁被设置在低3位时,锁要么偏向一个线程或者是匿名偏向,表明它有可能是被偏向状态。被偏向线程在锁定和解锁时可以在不使用原子操作的情况下由该线程执行。当偏向锁被撤销时,它会恢复到下面的正常状态描述的锁定方案。
[JavaThread* | epoch | age | 1 | 01] // 偏向锁偏向于一个线程,JavaThread*指向线程ID
[0 | epoch | age | 1 | 01] // 匿名偏向,即开启了偏向锁但是还没有被线程获取偏向锁
[ptr | 00] locked // ptr指针指向真实线程栈上的对象头
[header | 0 | 01] unlocked // 普通对象头
[ptr | 10] monitor // 膨胀锁,ptr指向监视器对象
[ptr | 11] marked // 由markSweep用于标记对象,在其他时间无效
读者大致看看对象头的描述即可,在了解了对象头之后,我们可以在下面的monitorenter和monitorexit 操作中看到,都是对于对象头部的这些信息进行灵活运用。
_monitorenter 获取锁过程原理
首先我们看一段最基本的Java通过synchronized关键字上锁的代码。
public class Demo{
static Object lock = new Object();
static int counter = 1;
public static void main(String[] args){
// 通过对lock对象上锁来保证代码块的操作的线程安全性
synchronized(lock){
counter++;
}
}
}
上面的代码很容易理解,我们直接来看编译的字节码。
0: getstatic #2 // Field lock:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter // 同步代码块开始
6: getstatic #3 // Field counter:I
9: iconst_1
10: iadd
11: putstatic #3 // Field counter:I
14: aload_1
15: monitorexit // 同步代码块结束
/* 发生异常后,跳转到第19行代码。不管是否发生异常都会释放锁对象。这里,如果没有发生异常,则直接跳转到24行,从main方法中返回。*/
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
从以上代码中可以看到,上锁过程是通过monitorenter字节码指令来操作的,这里直接跳到monitorenter的源码。
CASE(_monitorenter): {
// 获取锁对象
oop lockee = STACK_OBJECT(-1);
// 在线程栈上找到一个空闲的BasicObjectLock对象
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) {
// 保存锁对象,表明当前BasicObjectLock持有锁对象lockee
entry->set_obj(lockee);
int success = false;
uintptr_t epoch_mask_in_place = (uintptr_t)markOopDesc::epoch_mask_in_place;
markOop mark = lockee->mark(); // 获取锁对象的头部标记信息
// 获取没有hash值的标记位值,这里为0
intptr_t hash = (intptr_t) markOopDesc::no_hash;
// 判断使用了偏向锁
if (mark->has_bias_pattern()) {
uintptr_t thread_ident;
uintptr_t anticipated_bias_locking_value;
thread_ident = (uintptr_t)istate->thread(); // 获取线程id
anticipated_bias_locking_value =
(((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &
~((uintptr_t) markOopDesc::age_mask_in_place);
/* anticipated_bias_locking_value为0,表明还没有批量撤销偏向锁,且当前线程
持有了偏向锁,直接退出 */
if (anticipated_bias_locking_value == 0) {
// already biased towards this thread, nothing to do
if (PrintBiasedLockingStatistics) {
(* BiasedLocking::biased_lock_entry_count_addr())++;
}
success = true;
}
else if ((anticipated_bias_locking_value &
markOopDesc::biased_lock_mask_in_place) != 0) {
/* anticipated_bias_locking_value不为0,可能是批量撤销偏向锁,需要继续判断是否有
线程持有偏向锁,如果其他线程持有偏向锁,判定发生了冲突,就需要撤销偏向锁 */
markOop header = lockee->klass()->prototype_header();
if (hash != markOopDesc::no_hash) {
header = header->copy_set_hash(hash);
}
// CAS将对象头从mark替换为header撤销偏向锁
if (lockee->cas_set_mark(header, mark) == mark) {
if (PrintBiasedLockingStatistics)
(*BiasedLocking::revoked_lock_entry_count_addr())++;
}
}
else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {
/* 如果anticipated_bias_locking_value不为0,在批量撤销偏向锁时需要更改
epoch的值,这里如果epoch改变了,当前线程需要重偏向 */
markOop new_header = (markOop) ( (intptr_t) lockee->klass()->prototype_header() | thread_ident);
if (hash != markOopDesc::no_hash) {
new_header = new_header->copy_set_hash(hash);
}
// CAS重偏向
if (lockee->cas_set_mark(new_header, mark) == mark) {
if (PrintBiasedLockingStatistics)
(* BiasedLocking::rebiased_lock_entry_count_addr())++;
}
else {
// CAS失败,发生了竞争,那么进入monitorenter
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
success = true;
}
else {
/* 以上条件均不满足,表明开启了偏向锁,此时偏向锁状态为匿名偏向,尝试CAS
将其偏向为当前线程*/
markOop header = (markOop) ((uintptr_t) mark &
((uintptr_t)markOopDesc::biased_lock_mask_in_place |
(uintptr_t)markOopDesc::age_mask_in_place |
epoch_mask_in_place));
if (hash != markOopDesc::no_hash) {
header = header->copy_set_hash(hash);
}
markOop new_header = (markOop) ((uintptr_t) header | thread_ident);
// CAS重偏向
if (lockee->cas_set_mark(new_header, header) == header) {
if (PrintBiasedLockingStatistics)
(* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;
}
else {
// CAS失败,发生了竞争,那么进入monitorenter
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry),
handle_exception);
}
success = true;
}
}
// 没有获取到锁,那么进入传统的轻量级锁
if (!success) {
markOop displaced = lockee->mark()->set_unlocked();
entry->lock()->set_displaced_header(displaced);
bool call_vm = UseHeavyMonitors; // 判断是否直接使用重量级锁
/* 如果没有指定直接使用重量级锁,那么通过CAS操作尝试获取轻量级锁,即替换
头部指针,指向entry */
if (call_vm || lockee->cas_set_mark((markOop)entry, displaced) != displaced) {
// 如果失败,可能是当前线程轻量级锁重入,那么判断是否是锁重入
if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits()))
{
// 轻量级锁重入,不需要设置displaced_header信息
entry->lock()->set_displaced_header(NULL);
} else {
// 否则调用monitorenter
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry),
handle_exception);
}
}
}
UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
} else {
// 如果未找到,设置more_monitors标志位,由解释器分配新的BasicObjectLock并重试
istate->set_msg(more_monitors);
UPDATE_PC_AND_RETURN(0); // Re-execute
}
}
可以看到以上这段代码的核心就是BasicObjectLock对象,我们下面来看看这个对象和BasicLock的定义。
class BasicLock {
private:
volatile markOop _displaced_header; // 被替换的对象头部放在这里
};
class BasicObjectLock { // 对象处于栈帧中
private:
BasicLock _lock; // 基础锁对象
oop _obj; // 锁对象的引用
};
接下来,我们进入InterpreterRuntime::monitorenter中,来看看完整的加锁流程。
InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem){
/* 如果打开了JVM的PrintBiasedLockingStatistics的偏向锁分析,则在这里原子性增加
slow_path_entry_count的计数器 */
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj());
// 如果使用了偏向锁,则进入fast_enter,避免不必要的锁膨胀
if (UseBiasedLocking) {
ObjectSynchronizer::fast_enter(h_obj,elem->lock(),true,CHECK);// 进入快速模式
} else {
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK); // 进入慢速模式
}
}
下面来看ObjectSynchronizer::fast_enter。众所周知,所谓优化最后都会有兜底方案,当然,如果不可以快速进入,那么肯定里面还会调用ObjectSynchronizer::slow_enter。源码如下。
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS)
{
// 如果使用偏向锁,则尝试偏向
if (UseBiasedLocking) {
// 如果当前不在线程安全点,则尝试调用BiasedLocking::revoke_and_rebias进行偏向
if (!SafepointSynchronize::is_at_safepoint()) {
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj,
attempt_rebias, THREAD);
// 如果偏向成功直接返回
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
return;
}
} else {
// 否则表明在线程安全点中撤销偏向锁
BiasedLocking::revoke_at_safepoint(obj);
}
}
// 是不是和读者想的一样:快速不行,那就来慢速
slow_enter (obj, lock, THREAD) ;
}
下面先进入BiasedLocking::revoke_and_rebias,看看如何撤销并且重偏向,源码如下。
BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) {
// 获取到对象的mark头部
markOop mark = obj->mark();
// 当前对象为匿名偏向锁且不尝试重偏向
if (mark->is_biased_anonymously() && !attempt_rebias) {
markOop biased_value = mark; // 保存当前对象头部
/* 获取未偏向的头部,prototype()方法相当于获取初始状态下的对象头,源码:
markOop( no_hash_in_place | no_lock_in_place )。然后将当前对象的偏向锁头部的
年龄值复制给原始对象头 */
markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
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()) { // 当前对象是正常偏向锁
Klass* k = obj->klass(); // 获取当前对象的元数据klass对象
markOop prototype_header = k->prototype_header(); // 然后获得klass对象的原始头部
if (!prototype_header->has_bias_pattern()) { // 如果klass对象没有偏向锁
markOop biased_value = mark; // 保存当前对象头部
// 通过CAS操作将当前对象的头部替换为原始klass头部
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(prototype_header,
obj->mark_addr(), mark);
return BIAS_REVOKED; // 成功则直接返回
/* 如果原始klass的原始头部的偏向锁的epoch不等于当前对象的epoch,说明发生了重偏向
(每次偏向将会改变epoch的值)*/
} else if (prototype_header->bias_epoch() != mark->bias_epoch()) {
if (attempt_rebias) { // 如果尝试重偏向(从上面的代码进入这里传递的为true)
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); // 同样通过CAS替换
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) { // 如果决定不重偏向,则直接返回
return NOT_BIASED;
} else if (heuristics == HR_SINGLE_REVOKE) { // 如果只撤销当前对象的偏向锁,则直接撤销
Klass *k = obj->klass();
markOop prototype_header = k->prototype_header();
/* 如果当前偏向锁的偏向线程属于当前线程且epoch没有发生变换,则是当前线程可能
正在计算一个identity hash code值,所以需要撤销偏向锁。*/
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:");
}
BiasedLocking::Condition cond = revoke_bias(obj(), false, false, (JavaThread*) THREAD);
((JavaThread*) THREAD)->set_cached_monitor_info(NULL);
return cond;
} else { // 调用VMThread执行VM_RevokeBias撤销偏向锁
VM_RevokeBias revoke(&obj, (JavaThread*) THREAD);
VMThread::execute(&revoke);
return revoke.status_code();
}
}
// 上面的判断均是拥有偏向锁的状态,那么如果对象没有偏向锁呢
// 启发式算法必须等于批量重偏向或者批量撤销
VM_BulkRevokeBias bulk_revoke(&obj, (JavaThread*) THREAD,
(heuristics == HR_BULK_REBIAS),attempt_rebias);
// 这里直接调用VM线程来执行VM_BulkRevokeBias操作
VMThread::execute(&bulk_revoke);
// 返回执行结果
return bulk_revoke.status_code();
这里讲解的是偏向锁原理,所以为了方便读者学习,就不讲解批量撤销和批量重偏向了,只讲解撤销当前偏向锁的操作。建议读者在掌握撤销单个偏向锁后再继续跟进批量操作。下面来看启发值的更新算法update_heuristics。源码如下。
static HeuristicsResult update_heuristics(oop o,bool allow_rebias) {
markOop mark = o->mark(); // 获取当前对象的对象头
if (!mark->has_bias_pattern()) { // 如果当前对象没有偏向锁,则直接返回HR_NOT_BIASED
return HR_NOT_BIASED;
}
// 尝试限制批量撤销数量的启发式方法:
// 1.撤销该类型堆中所有对象的偏向锁,但如果这些对象是无锁状态,那么允许重偏向
// 2.撤销该类型堆中所有对象的偏向锁并且不允许这些对象重偏向。禁止分配对象时创建拥有偏向锁位的对象,也就是之后所有创建的新对象都不允许持有偏向锁
Klass* k = o->klass(); // 获取到klass对象
jlong cur_time = os::javaTimeMillis(); // 当前时间
jlong last_bulk_revocation_time = k->last_biased_lock_bulk_revocation_time();
// 获取klass对象最后一次偏向锁撤销时间
int revocation_count = k->biased_lock_revocation_count(); // 获取偏向锁撤销次数
/* 如果撤销次数大于或等于BiasedLockingBulkRebiasThreshold偏向锁批量重偏向次数,且撤销次数小于BiasedLockingBulkRevokeThreshold偏向锁批量撤销次数,以及last_bulk_revocation_time最后一次批量撤销时间不等于0,BiasedLockingDecayTime加上last_bulk_revocation_time时间小于当前时间,则表明了在一段时间内,一批相同类型的对象先偏向于一个线程,然后又重偏向了另外的线程。所以,这里通过批量重偏向的原理来保证效率,因此重置了klass对象的撤销计数器。*/
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_bias方法,源码如下。
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) {
// 如果允许重排向则恢复头部为未偏向状态
if (!allow_rebias) {
obj->set_mark(unbiased_prototype);
}
// 返回成功
return BiasedLocking::BIAS_REVOKED;
}
// 用于标记偏向的对象是否还在运行
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;
}
/* 到这里表明,偏向锁持有线程没有退出,如果当前线程持有偏向锁的话,则将替换的头部放到线程的堆栈中保存,否则还原对象头部为无锁状态或者无偏向锁状态 */
// 按年轻到老年的顺序获取当前锁定在当前线程的所有对象的监视器锁对象信息
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) {
//创建一个指向NULL的锁对象指针
markOop mark = markOopDesc::encode((BasicLock*) NULL);
// 获取锁对象BasicLock
highest_lock = mon_info->lock();
// 设置替换头部为NULL对象
highest_lock->set_displaced_header(mark);
}
}
/* 如果找到高位的锁对象,则将替换头部设置为未偏向头部,然后将当前对象的头部设置为锁对象
的头部 */
if (highest_lock != NULL) {
highest_lock->set_displaced_header(unbiased_prototype);
obj->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;
}
在了解完偏向锁撤销和重偏向之后,让我们回到最开始的faster_enter方法,代码如下。
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
if (UseBiasedLocking) {
if (!SafepointSynchronize::is_at_safepoint()) {
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) { // 注意,如果这里返回撤销并且是重偏向状态,那么直接返回。可以从上面的代码中看到,只有对象处于偏向锁状态且对象所属的klass对象头部的epoch和当前对象头部的epoch不相等,且attempt_rebias为true时,才会通过CAS替换,成功后才会返回BIAS_REVOKED_AND_REBIASED
return;
}
} else {
// 当在线程安全点时进行重偏向,这里不做过多解释
BiasedLocking::revoke_at_safepoint(obj);
}
}
slow_enter (obj, lock, THREAD) ; // 进入慢获取锁
}
偏向锁是针对原始的不常发生线程争用所提出的锁优化方案,如果发生线程争用,则会撤销偏向锁。如果是自身线程因为需要计算hashCode撤销,那么不需要进入线程安全点;如果不是,则需要进入线程安全点。这代价很高,因为需要所有线程全部停止,所以JVM提出了批量撤销和重偏向来优化。下面我们来看看slow_enter的操作。
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
// 获取上锁对象头部标记信息
markOop mark = obj->mark();
// 如果对象处于无锁状态
if (mark->is_neutral()) {
// 将对象头部保存在lock对象中
lock->set_displaced_header(mark);
// CAS尝试替换对象头部为lock对象地址,如果替换成功,则直接返回,表明获取了轻量级锁
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
return ;
}
// 否则判断当前对象是否有上锁,并且当前线程是否是锁的占有者
} else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
// 设置lock对象的替换头部为NULL后直接返回
lock->set_displaced_header(NULL);
return;
}
lock->set_displaced_header(markOopDesc::unused_mark()); /* 如果走到这一步说明对象被其他线程上锁了。对象头永远不会被替换到这个锁,所以值是什么并不重要,重要的是它必须是非零的,以避免看起来像重入锁,而且也不能看起来像被锁住一样,所以这里给了个未使用的标记位:marked_value = 3(后三位为011)*/
ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD); // 开始锁膨胀
}
轻量级锁的实现便是BasicLock对象,这个对象在线程栈上,里面保存有替换了的对象头部。但是如果对象已经被其他线程上锁,那么这个对象将不再使用。直接给一个临时的未使用的值填充displaced_header,接下来直接进入inflate膨胀为重量级锁。下面我们来看看ObjectSynchronizer::inflate (THREAD, obj())->enter(THREAD)。这里分两步,即获取监视器和enter,先看获取监视器ObjectSynchronizer::inflate。源码如下。
ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
for (;;) { // 标准操作,读者再看原子类时已经了解到,包括AQS里大量使用死循环
const markOop mark = object->mark() ; // 首先获取对象的标记头
// 对象标记位可能处于以下任意一个状态:
// * Inflated:如果已经膨胀为重量级锁,则直接返回
// Stack-locked:如果对象处于轻量级锁状态,直接强制膨胀为重量级锁
// INFLATING:如果正在膨胀中,那么等待膨胀完成,这就是for死循环存在的意义
// Neutral:如果是无锁对象直接膨胀为重量级锁
// BIASED:偏向锁,这里属于非法状态,不允许出现
// 第一种情况:已经膨胀为重量级锁
if (mark->has_monitor()) { // 直接通过(value() & monitor_value) != 0判断是
// 否lock标记位为10,
// 即直接将value()与2判断是否为0
ObjectMonitor * inf = mark->monitor() ; // 直接获取到monitor对象
return inf ;
}
// 第二种情况:对象正在被其他线程从轻量级锁膨胀为重量级锁。当然,只有一个线程能够进行膨胀,其
// 他线程想要膨胀,就需要等待。INFLATING 状态是易变的,因为只要膨胀完成会马上修改。当前采取自
// 旋/yield让出CPU/park等待并且轮询markword直到膨胀完成。当然,也可以让线程停在一些辅助列表
// 上来消除轮询。其实就是膨胀完成一起唤醒等待线程
if (mark == markOopDesc::INFLATING()) {
// 这里我们一会儿再说,其实就是等待优化
ReadStableMark(object) ;
continue ;
}
// 第三种情况:对象当前处于轻量级锁状态,即stack-locked状态。可能被当前线程上了轻量级锁,也可
// 能被其他线程上了轻量级锁。在尝试把INFLATING设置在markword之前,这里先分配了监视器对象。
// 每个线程都有私有的监视器对象列表,线程可以从全局的free list列表中转移到线程私有列表。这样就减
// 少了全局free list的竞争和缓存一致性冲突。
if (mark->has_locker()) {
ObjectMonitor * m = omAlloc (Self) ; // 分配监视器,这又是个强大的方法,一会儿再讲,只需要
// 知道这里分配了监视器对象。预先分配监视器是为了减少在CAS成功后分配监视器的时间,这样会
// 导致INFLATING等待时间过长
m->Recycle(); // 这里初始化成员变量
m->_Responsible = NULL ;
m->OwnerIsThread = 0 ;
m->_recursions = 0 ;
m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; // 监视器自旋限制为5000
markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(),mark) ; // 准备好监视器后,尝试通过CAS将对象头部标记修改为INFLATING
if (cmp != mark) { // 如果替换失败,则释放监视器对象,继续重试
omRelease (Self, m, true) ;
continue ;
}
// 到这一步,已经成功地将INFLATING放到了对象头中。
// 这里调用value() & ~monitor_value去掉监视器值,也就是(10)状态位的markOop
markOop dmw = mark->displaced_mark_helper() ;
// 把去掉锁标志位的markOop赋值给监视器对象头部
m->set_header(dmw) ;
// 设置监视器拥有者为当前对象所持有的BasicLock,也就是标明了哪个线程占有锁,因为BasicLock
// 是某个线程栈上的对象
m->set_owner(mark->locker());
// 设置拥有监视器监视对象为当前传入对象
m->set_object(object);
// 将object的头部监视器位放上监视器的地址
object->release_set_mark(markOopDesc::encode(m));
// 最后返回监视器
return m ;
}
// 第四种情况:正常未上锁对象
// 同样预分配监视器
ObjectMonitor * m = omAlloc (Self) ;
// 初始化监视器
m->Recycle();
m->set_header(mark);
m->set_owner(NULL);
m->set_object(object);
// 这个状态用来区别监视器拥有者是线程指针还是BasicLock的指针,这里设置为1表明为Thread指针
m->OwnerIsThread = 1 ;
// 重入次数
m->_recursions = 0 ;
m->_Responsible = NULL ;
m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;
// 尝试通过CAS替换对象头部为监视器的地址,如果替换失败,则释放监视器,然后重试
if (Atomic::cmpxchg_ptr (markOopDesc::encode(m),object->mark_addr(),mark) != mark) {
m->set_object (NULL) ;
m->set_owner (NULL) ;
m->OwnerIsThread = 0 ;
m->Recycle() ;
omRelease (Self, m, true) ;
m = NULL ;
continue ;
}
return m ; // 这里就是CAS成功后的监视器锁对象
}
}
for死循环的强大,就是在状态不成立时重试,这里只会出现4种情况:已经膨胀为重量级锁(即对象头部拥有指针指向重量级锁对象)、正在膨胀中、原来已经加上了轻量级锁现在需要膨胀、无锁对象。而最终目的都只有一个,即将对象头部放入监视器对象地址。这里还有ReadStableMark(用于线程等待优化)和omAlloc(用于分配监视器对象)两个核心方法没有讨论。我们先来看看ReadStableMark。
static markOop ReadStableMark (oop obj) {
// 同样获取对象头部快照
markOop mark = obj->mark() ;
// 如果已经膨胀为重量级锁,则判断是否为0,如果已经膨胀,则直接快速返回
if (!mark->is_being_inflated()) {
return mark ;
}
int its = 0 ; // 计数变量
for (;;) { // 死循环等待
markOop mark = obj->mark() ; // 再次获取对象头部
if (!mark->is_being_inflated()) {
// 如果已经膨胀为重量级锁了,则判断是否为0,如果已经膨胀,则直接快速返回
return mark ;
}
++its;
// 如果计数变量大于10000或者运行的机器不是多处理器
if (its > 10000 || !os::is_MP()) {
// 如果计数变量为基数,那么直接调用Yield让出CPU
if (its & 1) {
os::NakedYield() ;
} else {
int ix = (cast_from_oop<intptr_t>(obj) >> 5) & (NINFLATIONLOCKS-1) ; // 获得索引
int YieldThenBlock = 0 ;
Thread::muxAcquire (InflationLocks + ix, "InflationLock") ; //获取锁,这个锁保证了不会产生
// 太多线程自旋或者park(1)导致系统变慢,这里的值默认为256,如果获取不到锁,或直接park直到锁释放
//当前对象头部正在膨胀阶段时
while (obj->mark() == markOopDesc::INFLATING()) {
if ((YieldThenBlock++) >= 16) {
Thread::current()->_ParkEvent->park(1) ; // 调用park或者Yield
} else {
os::NakedYield() ;
}
}
Thread::muxRelease (InflationLocks + ix ) ; // 释放锁
}
} else {
// 如果计数变量小于10000或者对称多处理器架构,则直接循环自旋等待
SpinPause() ;
}
}
}
接下来,我们来看看omAlloc方法的实现。源码如下。
ObjectMonitor * ATTR ObjectSynchronizer::omAlloc (Thread * Self) {
// 一个较大的MAXPRIVATE值可以减少列表锁争用和缓存一致性冲突,但也会增加列表中的ObjectMonitor的数量以及STW清除成本。通常我们会在时间和空间中找到一个平衡
const int MAXPRIVATE = 1024 ; // 线程最大私有监视器个数
for (;;) { // 标注操作
ObjectMonitor * m ;
// 先尝试从线程的omFreeList空闲链表中分配,这个变量属于线程Thread类的一个局部变量ObjectMonitor* omFreeList,这是个监视器链表,对象指针指向链表头部
m = Self->omFreeList ;
if (m != NULL) { // 如果omFreeList列表有监视器对象
Self->omFreeList = m->FreeNext ; // 将m的下一个监视器对象赋值给omFreeList
Self->omFreeCount -- ; // omFreeList 数量减1
if (MonitorInUseLists) { // 如果设置了MonitorInUseLists标识
// 将当前分配到的ObjectMonitor 放到监视器使用链表上
m->FreeNext = Self->omInUseList;
Self->omInUseList = m;
Self->omInUseCount ++;
// verifyInUse(Self);
} else {
m->FreeNext = NULL;
}
return m ;
}
// 然后尝试从全局链表中分配
// 如果全局空闲链表不为NULL
if (gFreeList != NULL) {
Thread::muxAcquire (&ListLock, "omAlloc") ; // 获取到空闲链表锁
// 批量获取锁对象减少分配线程间竞争,获取的数量为omFreeProvision设置的数量
for (int i = Self->omFreeProvision; --i >= 0 && gFreeList != NULL; ) {
MonitorFreeCount --;
ObjectMonitor * take = gFreeList ;
gFreeList = take->FreeNext ;
take->Recycle() ; // 初始化监视器变量
omRelease (Self, take, false) ; // 将全局空闲监视器放到本地线程空闲链表中
}
Thread::muxRelease (&ListLock) ; // 释放锁
// 缓慢增加批量获取的大小
Self->omFreeProvision += 1 + (Self->omFreeProvision/2) ;
// 如果超过了MAXPRIVATE,则omFreeProvision设置等于MAXPRIVATE
if (Self->omFreeProvision > MAXPRIVATE ) Self->omFreeProvision = MAXPRIVATE ;
const int mx = MonitorBound ;
if (mx > 0 && (MonitorPopulation-MonitorFreeCount) > mx) {
// 大于设定监视器大小,则诱发垃圾回收释放监视器对象
InduceScavenge (Self, "omAlloc") ;
}
// 从全局对象中分配过来后,重试分配
continue;
}
// 监视器分配数量默认为128
ObjectMonitor * temp = new ObjectMonitor[_BLOCKSIZE];
// 如果分配失败,可能由于操作系统内存不足,那么将抛出异常
if (temp == NULL) {
vm_exit_out_of_memory (sizeof (ObjectMonitor[_BLOCKSIZE]),
OOM_MALLOC_ERROR, "Allocate ObjectMonitors");
}
// 将分配的一组监视器对象初始化,使它们链表化
for (int i = 1; i < _BLOCKSIZE ; i++) {
temp[i].FreeNext = &temp[i+1];
}
// 将监视器数组的最后一个监视器设置为NULL,因为上一步产生了数组越界,最后多加了1位
temp[_BLOCKSIZE - 1].FreeNext = NULL ;
// 第一个监视器对象为全局列表链接保留
temp[0].set_object(CHAINMARKER);
// 获取全局链表的锁
Thread::muxAcquire (&ListLock, "omAlloc [2]") ;
// 增加MonitorPopulation和MonitorFreeCount数量
MonitorPopulation += _BLOCKSIZE-1;
MonitorFreeCount += _BLOCKSIZE-1;
// 将第一监视器对象作为全局链表对象使用
temp[0].FreeNext = gBlockList;
// 最后将temp赋值给gBlockList,这样这个新分配的监视器数组的第一个监视器就作为了全局监视器对
// 象的根对象
gBlockList = temp;
// 将数组最后一个监视器链到全局监视器链表上
temp[_BLOCKSIZE - 1].FreeNext = gFreeList ;
gFreeList = temp + 1;
// 最后释放锁,然后继续循环分配
Thread::muxRelease (&ListLock) ;
}
}
到此为止,我们把ObjectSynchronizer::inflate(THREAD, obj())讲完了。接下来,我们来看ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD),看看在获取到监视器对象后应该怎么做。源码如下。
void ATTR ObjectMonitor::enter(TRAPS) {
Thread * const Self = THREAD ;
void * cur ;
// CAS将线程对象替换到ObjectMonitor上
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
if (cur == NULL) { // 如果替换成功,表明获得锁
return ;
}
// 如果替换失败,则先判断持有监视器锁的线程是不是当前线程,如果是,则直接增加重入计数
if (cur == Self) {
_recursions ++ ;
return ;
}
// 如果当前线程是对象的轻量级锁拥有者,即BasicLock,则将头部替换为BasicLock的指针
if (Self->is_lock_owned ((address)cur)) {
_recursions = 1 ; // 首先将重入次数置为1
_owner = Self ; // 将指向BasicLock改变为指向线程本身
OwnerIsThread = 1 ; // 修改线程标识位
return ; // 最后直接返回
}
// 到这一步,就可判定有多个线程在互相争用这个监视器了
// 将监视器this指针保留在当前Thread对象中
Self->_Stalled = intptr_t(this) ;
// 先尝试一轮自旋,如果在自旋状态获得锁,将不发布JVMTI事件和触发DTRACE探针
// 如果开启Knob_SpinEarly选项且TrySpin大于0,则表明获取了监视器锁对象,直接返回
if (Knob_SpinEarly && TrySpin (Self) > 0) {
Self->_Stalled = 0 ;
return ;
}
// 向下转型为JavaThread对象
JavaThread * jt = (JavaThread *) Self ;
Atomic::inc_ptr(&_count); // 原子性增加_count地址指针,防止在GC STW时这个监视器被回收,由于本书的重点不是讲解垃圾回收,因此这里不做详述,并不影响读者理解上锁流程
EventJavaMonitorEnter event;
{ // 更改Java线程状态表明阻塞在monitor enter处
JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this);
// JavaThread关联的OSthread的状态
OSThreadContendState osts(Self->osthread());
ThreadBlockInVM tbivm(jt);
// 将当前监视器保存到线程的正在等待的监视器中
// 死循环,常规操作,直到获得锁退出
for (;;) {
Self->set_current_pending_monitor(this);
// 设置suspend暂停条件为true
jt->set_suspend_equivalent();
EnterI (THREAD) ;
// 如果标志位suspend为false,则退出
if (!ExitSuspendEquivalent(jt)) break ;
/* 此时事实上已获得了监视器锁,但suspend状态还是为false,不允许获得锁,
必须释放掉获得的监视器锁 */
_recursions = 0 ;
_succ = NULL ;
// 释放监视器
exit (false, Self) ;
// 调用这个方法让线程suspend,直到其他线程唤醒它,然后继续使用for循环竞争锁
jt->java_suspend_self();
}
// 获得锁后设置等待监视器为NULL
Self->set_current_pending_monitor(NULL);
}
// 还原增加的指针
Atomic::dec_ptr(&_count);
Self->_Stalled = 0 ;
}
这段代码相对简单,无非就是先CAS当前线程指针和监视器的_owner替换试试,失败就看看是不是当前线程占用了监视器,如果是就是重入锁+1,再开始竞争,分为自旋竞争TrySpin (Self)和等待竞争EnterI (THREAD)。这里别急,先一个一个来。下面看看TrySpin 是如何实现的。源码如下。
#define TrySpin TrySpin_VaryDuration
// 采用自适应自旋锁来保证性能
int ObjectMonitor::TrySpin_VaryDuration (Thread * Self) {
// 如果设置了固定旋转,那么这里直接旋转固定次数
int ctr = Knob_FixedSpin;
if (ctr != 0) {
while (--ctr >= 0) {
/* tryLock其实就是上面看到的监视器在进入的第一个条件:CAS替换_owner字段,
替换成功,返回1;否则返回-1。一直重试,直到自旋次数到达或获得了锁 */
if (TryLock (Self) > 0) return 1 ;
// 这里在不同系统上有不同的实现,基本都是return 1,相当于一个空操作
SpinPause () ;
}
return 0 ;
}
// 首先预自旋Knob_PreSpin,Knob_PreSpin默认为20
for (ctr = Knob_PreSpin + 1; --ctr >= 0 ; ) {
// 如果获得了锁,增加_SpinDuration 的值
if (TryLock(Self) > 0) {
int x = _SpinDuration ;
// Knob_SpinLimit默认等于5000
if (x < Knob_SpinLimit) {
// Knob_Poverty默认为1000
if (x < Knob_Poverty) x = Knob_Poverty ;
// Knob_BonusB默认为100
_SpinDuration = x + Knob_BonusB ;
}
return 1 ;
}
SpinPause () ;
}
// 设置ctr为当前 _SpinDuration值
ctr = _SpinDuration ;
// ctr最小为Knob_SpinBase
if (ctr < Knob_SpinBase) ctr = Knob_SpinBase ;
// 如果ctr小于或等于0,则直接返回,毕竟空旋转没意义
if (ctr <= 0) return 0 ;
// 如果限制了successors且_succ不为空,则直接返回,_succ指明了后面需要唤醒的线程
if (Knob_SuccRestrict && _succ != NULL) return 0 ;
// 如果开启了Knob_OState检测线程状态,那么进入NotRunnable (Self, (Thread *) _owner)进行判断,如果返回值不等于0,则直接退出,自旋终止
if (Knob_OState && NotRunnable (Self, (Thread *) _owner)) {
return 0 ;
}
//最大允许自旋的线程数,默认为-1
int MaxSpin = Knob_MaxSpinners ;
// 如果指定了允许自旋的最大线程数,那么进行判断是否超过,如果超过了这个数,直接返回
if (MaxSpin >= 0) {
if (_Spinner > MaxSpin) {
return 0 ;
}
// 调整自旋线程数数量,这里表明原子加1
Adjust (&_Spinner, 1) ;
}
// 开始自旋
int hits = 0 ;
int msk = 0 ;
int caspty = Knob_CASPenalty ; // CAS失败后的处罚值默认为-1
int oxpty = Knob_OXPenalty ; // 观察到监视器的_owner更改处罚默认为-1
int sss = Knob_SpinSetSucc ; // 自旋线程是否可以设置_succ线程,默认开启为1
if (sss && _succ == NULL ) _succ = Self ; //如果开启sss且_succ为NULL,则初始化_succ为线程自身
Thread * prv = NULL ;
// 有3种方式退出下面的循环:
// 1. 当前线程在自旋中成功获得了锁
// 2. 带有“偏见”的自旋锁失败,即对自旋次数进行了削减
// 3. 不带有“偏见”的自旋锁失败,即没有改变自旋次数
while (--ctr >= 0) { // 最大自旋次数
/* 周期性检查是否到达线程安全点,即判断线程安全点状态_state != _not_synchronized,
如果到达线程安全点,那么直接中断线程 */
if ((ctr & 0xFF) == 0) {
if (SafepointSynchronize::do_call_back()) {
goto Abort ;
}
// 如果开启了SpinPause ,那么调用SpinPause暂停自旋
if (Knob_UsePause & 1) SpinPause () ;
// 自旋回调函数默认为空
int (*scb)(intptr_t,int) = SpinCallbackFunction ;
if (hits > 50 && scb != NULL) {
int abend = (*scb)(SpinCallbackArgument, 0) ;
}
}
if (Knob_UsePause & 2) SpinPause() ;
// 如果ctr与msk不等于0,继续循环自旋
if (ctr & msk) continue ;
++hits ; // 自增hits
// 如果hits的计数后四位为0,即与0x00001111时为0,也就对应于指数运算
// 将msk左移两位或011,再与BackOffMask默认0,所以这里的操作相当于无操作,任何数与0都为0
if ((hits & 0xF) == 0) { msk = ((msk << 2)|3) & BackOffMask ;
}
// 获取该监视器的拥有线程
Thread * ox = (Thread *) _owner ;
if (ox == NULL) {
// 尝试通过CAS替换_owner为当前线程
ox = (Thread *) Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
// 如果替换成功,则表明获得锁
if (ox == NULL) {
//如果开启Knob_SpinSetSucc,默认为1表明开启,且_succ等于自身线程,则清除_succ位
if (sss && _succ == Self) {
_succ = NULL ;
}
// 如果设定了自旋线程限制,那么减1,因为这里已经成功自旋获得锁
if (MaxSpin > 0) Adjust (&_Spinner,-1) ;
int x = _SpinDuration ;
// 在没有到达自旋次数限制的同时,适当增加自旋的次数,因为这里自旋成功了,表明自旋还是
// 有效果的,适当增加可以提高获取锁的概率
if (x < Knob_SpinLimit) {
if (x < Knob_Poverty) x = Knob_Poverty ;
_SpinDuration = x + Knob_Bonus ;
}
return 1 ;
}
// CAS替换失败,这里可以采取以下任何一个操作:
// 处罚: ctr -= Knob_CASPenalty
// 带着“偏见”的退出循环 -- goto Abort;
// 不带“偏见”的退出
// 因为CAS是从CPU到内存,需要经过总线,具有较大延迟,那么立即继续循环自旋
prv = ox ; // 将上一个锁的拥有线程保存到prv中,也就是prev的简写
// 如果Knob_OXPenalty为 -2,则直接跳出循环,但是这个值默认为-1
if (caspty == -2) break ;
// 这里观察到_owner变换了,所以带有“偏见”的调用Abort退出循环
if (caspty == -1) goto Abort ;
// 否则让ctr减掉caspty的值后继续执行
ctr -= caspty ;
continue ;
}
// 判断锁的线程是否发生切换,如果观察到发生切换,则做出相应惩罚,根据Knob_OXPenalty的值来判断
if (ox != prv && prv != NULL ) {
if (oxpty == -2) break ;
if (oxpty == -1) goto Abort ;
ctr -= oxpty ;
}
prv = ox ; // 保存上一个锁拥有者
/* 如果开启了锁状态检查,默认为3表明开启,且当前持有锁的线程没有处于Runnable状态,
那么要立即结束,因为自旋等待一个非执行状态的线程是没有意义的 */
if (Knob_OState && NotRunnable (Self, ox)) {
goto Abort ;
}
if (sss && _succ == NULL ) _succ = Self ;
}
{
int x = _SpinDuration ;
// 自旋失败了,表明自旋对于提升性能没多大帮助,那么逐渐减少自旋次数以作惩罚
if (x > 0) {
x -= Knob_Penalty ;
if (x < 0) x = 0 ;
_SpinDuration = x ;
}
}
Abort:
if (MaxSpin >= 0) Adjust (&_Spinner, -1) ; // 调整参数
if (sss && _succ == Self) {
_succ = NULL ;
OrderAccess::fence() ; // 使用了全屏障避免指令的重排序,在tryLock时保证顺序
if (TryLock(Self) > 0) return 1 ; // 再一次尝试获取锁
}
return 0 ;
}
通过这段代码我们了解到,在平衡性能方面,开发人员无所不用其极。因为考虑到SMP和单处理的结构,自旋锁的性能不一定比park线程的开销小,CAS操作特别占用总线,并且对于缓存行还不友好,每次都得从内存中获取值。所以这段代码也一直都在平衡二者之间的差距,提出了自适应自旋锁。那么接下来,我们来分析在自旋失败后的一系列操作EnterI (THREAD)。源码如下。
void ATTR ObjectMonitor::EnterI (TRAPS) {
Thread * Self = THREAD ;
// 开始之前先尝试获取锁
if (TryLock (Self) > 0) {
return ;
}
DeferredInitialize () ; // 初始化监视器,这个不影响流程,一会儿讲解
/* 先尝试自旋,还记得之前在调用这个方法时有个判断吗?if (Knob_SpinEarly && TrySpin (Self) > 0),这边的开关就是Knob_SpinEarly是否允许提前自旋,默认是1 */
if (TrySpin (Self) > 0) {
return ;
}
// 自旋失败,那么开始进入等待队列,并且开始阻塞线程
// 把当前线程放入监视器的_cxq竞争队列中
// 队列节点充当当前线程的代理,熟悉AQS的读者,可以发现这里有异曲同工之妙
ObjectWaiter node(Self) ; // 创建等待节点
Self->_ParkEvent->reset() ; // 初始化ParkEvent
node._prev = (ObjectWaiter *) 0xBAD ; // 设置prev节点为BAD地址,即一个空对象
// 设置节点状态为CXQ状态,表明放入_cxq队列
node.TState = ObjectWaiter::TS_CXQ ;
// 将线程push到cxq的头部
// 一旦线程处于cxq或者entrylist上,线程就会一直在队列上,直到它获得了监视器的锁
ObjectWaiter * nxt ;
for (;;) { // 死循环常规操作
node._next = nxt = _cxq ; // 设置node的next为_cxq,即头插法,每次插在头部
// CAS插入,如果插入成功直接退出循环
if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;
// CAS失败,表明有线程竞争成功,这里通过优化尝试获取锁,如果不行,则继续CAS
if (TryLock (Self) > 0) {
return ;
}
}
/* 当nxt节点为空且_EntryList也为空,表明当前只有这个线程正在等待锁,因为nxt是跟着
_cxq走的,想象一下,如果这个线程把自己插入了头部,即_cxq就等于这个线程,
但是nxt为null表明后面没有线程了,谁来唤醒这个线程呢?所以这里检测边界条件 */
if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) {
// CAS尝试将当前线程作为线程_Responsible角色
Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
}
int nWakeups = 0 ;
int RecheckInterval = 1 ;
for (;;) { // 再次进入死循环,直到获得锁才会退出循环
if (TryLock (Self) > 0) break ; // 先尝试获取锁
// _owner不能为当前线程,因为上面都获取失败了,为什么监视器持有对象会是这个线程
if ((SyncFlags & 2) && _Responsible == NULL) {
Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
}
// 将线程park阻塞
// 如果_Responsible为当前线程且SyncFlags最后一位为1,这个参数默认为0
if (_Responsible == Self || (SyncFlags & 1)) {
// 调用ParkEvent 阻塞RecheckInterval时间
Self->_ParkEvent->park ((jlong) RecheckInterval) ;
RecheckInterval *= 8 ; // 逐渐增加park时间
if (RecheckInterval > 1000) RecheckInterval = 1000 ; // 最大为1000ms
} else {
// 到这一步表明后面有等待线程会负责通知自己,直接永久park阻塞,直到被唤醒
Self->_ParkEvent->park() ;
}
if (TryLock(Self) > 0) break ; // 唤醒后尝试获取锁
// 到这一步,锁仍然被争用,只需要记录下无用的唤醒次数即可
if (ObjectMonitor::_sync_FutileWakeups != NULL) {
ObjectMonitor::_sync_FutileWakeups->inc() ;
}
++ nWakeups ;
// 如果开启了Knob_SpinAfterFutile,即被无用唤醒后是否自旋
if ((Knob_SpinAfterFutile & 1) && TrySpin (Self) > 0) break ;
// 这个参数默认关闭,不用管,也就是判断是否重置ParkEvent
if ((Knob_ResetEvent & 1) && Self->_ParkEvent->fired()) {
Self->_ParkEvent->reset() ;
OrderAccess::fence() ;
}
// 如果_succ等于当前线程,那么赋值为NULL
if (_succ == Self) _succ = NULL ;
// 清空_succ后线程必须重试获取锁,所以又用了全屏障来保证指令顺序
// 继续循环获取锁
OrderAccess::fence() ;
}
// 这一步表明线程获得了监视器的锁
UnlinkAfterAcquire (Self, &node) ; // 获取锁后断开链表
if (_succ == Self) _succ = NULL ;
if (_Responsible == Self) {
_Responsible = NULL ;
OrderAccess::fence();
}
if (SyncFlags & 8) {
OrderAccess::fence() ;
}
return ;
}
从上面代码中可以看到有3个关键点,即DeferredInitialize延迟初始化监视器、ParkEvent->park()暂停线程、UnlinkAfterAcquire断开链接。其实读者不难发现,从自旋锁到监视器锁内部的ParkEvent也是在尽力的保障性能,在让线程通过操作系统阻塞之前,总是尽量通过自旋来保证不会发生频繁的上下文切换,而且也考虑到了边界条件,即entrylist为空且cxq竞争队列只有当前线程的情况。这就告诉了我们,在编码时对于边界条件的判断不能马虎,不然总是会出现一些难以预料的bug。这里或许有读者会困惑,cxq队列和entrylist队列到底是什么?读者大可不必着急,只需要知道线程包装成ObjectMonitor后就会通过CAS头插法放入cxq队列中,然后调用ParkEvent阻塞即可。至于entrylist,读者继续往下看就知道了,其是个优化算法,没有cxq只有entrylist也行的,但考虑到了性能问题,所以分开成了两个队列。这里我们先来看DeferredInitialize的实现。源码如下。
void ObjectMonitor::DeferredInitialize () {
if (InitDone > 0) return ; // 如果已经初始化完成直接退出
/* 通过CAS 将状态修改为-1,表明正在初始化。这里稍有不规范,应该把魔术变量,
即形如 -1 0 1的状态位,声明为变量 */
if (Atomic::cmpxchg (-1, &InitDone, 0) != 0) {
while (InitDone != 1) ; // 因为初始化很快,所以直接循环等待完成即可
return ;
}
// 开始初始化
if (SyncKnobs == NULL) SyncKnobs = "" ;
size_t sz = strlen (SyncKnobs) ;
char * knobs = (char *) malloc (sz + 2) ; // 调用原生C函数分配SyncKnobs+2字节大小的空间
if (knobs == NULL) { // 如果分配失败,则表明OS内存不足,报错退出虚拟机
vm_exit_out_of_memory (sz + 2, OOM_MALLOC_ERROR, "Parse SyncKnobs") ;
}
strcpy (knobs, SyncKnobs) ; // 将SyncKnobs的值复制给knobs
knobs[sz+1] = 0 ; // 由于上面多分配了两个字节,因此初始化为0
for (char * p = knobs ; *p ; p++) {
if (*p == ':') *p = 0 ; // 循环遍历开辟的字符数组,将值为“:”初始化为0
}
// 利用宏定义初始化所有变量
#define SETKNOB(x) { Knob_##x = kvGetInt (knobs, #x, Knob_##x); }
SETKNOB(ReportSettings) ;
SETKNOB(Verbose) ;
SETKNOB(FixedSpin) ;
SETKNOB(SpinLimit) ;
SETKNOB(SpinBase) ;
SETKNOB(SpinBackOff);
SETKNOB(CASPenalty) ;
SETKNOB(OXPenalty) ;
SETKNOB(LogSpins) ;
SETKNOB(SpinSetSucc) ;
SETKNOB(SuccEnabled) ;
SETKNOB(SuccRestrict) ;
SETKNOB(Penalty) ;
SETKNOB(Bonus) ;
SETKNOB(BonusB) ;
SETKNOB(Poverty) ;
SETKNOB(SpinAfterFutile) ;
SETKNOB(UsePause) ;
SETKNOB(SpinEarly) ;
SETKNOB(OState) ;
SETKNOB(MaxSpinners) ;
SETKNOB(PreSpin) ;
SETKNOB(ExitPolicy) ;
SETKNOB(QMode);
SETKNOB(ResetEvent) ;
SETKNOB(MoveNotifyee) ;
SETKNOB(FastHSSEC) ;
#undef SETKNOB
if (os::is_MP()) { // 当前环境为多处理器架构
BackOffMask = (1 << Knob_SpinBackOff) - 1 ;
if (Knob_ReportSettings) ::printf ("BackOffMask=%X", BackOffMask) ;
// CONSIDER: BackOffMask = ROUNDUP_NEXT_POWER2 (ncpus-1)
} else { // 单处理器自旋起不到优化性能的作用,直接关闭,下面的变量之前说过
Knob_SpinLimit = 0 ;
Knob_SpinBase = 0 ;
Knob_PreSpin = 0 ;
Knob_FixedSpin = -1 ;
}
if (Knob_LogSpins == 0) {
ObjectMonitor::_sync_FailedSpins = NULL ;
}
// 这些空间里的键值对已通过宏定义赋给了监视器,这里不需要保存,直接释放掉
free (knobs) ;
/* 通过全屏障指令保证指令顺序,即后面的InitDone不会重排序到前面的变量之前,如果
不这么做,一旦CPU指令重排序到这些变量还没有初始化之前,就把InitDone设置为1,
会导致程序崩溃 */
OrderAccess::fence() ;
InitDone = 1 ;
}
从上面代码中我们看到,最终通过线程对象的ParkEvent->park()阻塞线程,读者这里只需要知道通过这个结构体阻塞了线程即可,因为在后面的内容中将详细分析Parker和ParkEvent的实现原理和机制。 接下来,我们看看UnlinkAfterAcquire的实现。源码如下。
void ObjectMonitor::UnlinkAfterAcquire (Thread * Self, ObjectWaiter * SelfNode)
{
/* 如果线程状态为TS_ENTER,则表明当前线程阻塞在entrylist中,直接从entrylist中断开链接,
由于entrylist是安全无竞争的,因此这里直接无锁化操作 */
if (SelfNode->TState == ObjectWaiter::TS_ENTER) {
ObjectWaiter * nxt = SelfNode->_next ;
ObjectWaiter * prv = SelfNode->_prev ;
if (nxt != NULL) nxt->_prev = prv ;
if (prv != NULL) prv->_next = nxt ;
if (SelfNode == _EntryList ) _EntryList = nxt ;
} else {
// 否则线程阻塞在cxq竞争队列中
ObjectWaiter * v = _cxq ; // 获取当前cxq队列头部的快照
/* 如果当前cxq队列的头部等待线程不是当前线程,或者原子性的将当前节点的下一个节点
替换为cxq头节点失败,表明有线程在竞争cxq队列 */
if (v != SelfNode || Atomic::cmpxchg_ptr (SelfNode->_next, &_cxq, v) != v) {
/* 判断v是否为当前线程节点,如果v指针指向的监视器节点等于当前节点的话,由于
上面的CAS操作失败了,表明有线程在CAS头插cxq队列,那么当前节点不可能在
头节点的位置,因此在此将cxq的头部保存在v局部变量上 */
if (v == SelfNode) {
v = _cxq ; // CAS 替换失败后,保存当前cxq头部快照
}
ObjectWaiter * p ;
ObjectWaiter * q = NULL ;
// 从头节点开始向后遍历,直到找到等待节点等于当前线程的节点
for (p = v ; p != NULL && p != SelfNode; p = p->_next) {
q = p ;
}
/* 由于线程CAS头插,所以当前节点和后面节点的操作是线程安全的,这里直接修改
链接,将当前节点的上一个节点链接到当前节点的下一个节点,即可断开链接 */
q->_next = p->_next ;
}
}
// 修改当前节点的prev和next指针为一个BAD值,并且修改状态为TS_RUN
SelfNode->_prev = (ObjectWaiter *) 0xBAD ;
SelfNode->_next = (ObjectWaiter *) 0xBAD ;
SelfNode->TState = ObjectWaiter::TS_RUN ;
}
小结
偏向锁是HotSpot 虚拟机使用的一项优化技术,能够减少无竞争锁定时的开销。 偏向锁的目的是假定monitor 一直由某个特定线程持有,直到另一个线程尝试获取它,这样就可以避免获取monitor 时执行cas 的原子操作。 monitor 首次锁定时偏向该线程,这样就可以避免同一对象的后续同步操作步骤需要原子指令。
轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。
重量级锁显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。
参考书籍
《深入理解Java高并发编程》
扫码购买
精彩回顾
扫码关注《Java学研大本营》