本篇博文将分析Java同步锁的实现和演变,包括偏向锁、轻量级锁、重量级锁。
什么是重量级锁?
重量级锁是一种同步机制,通常与在多线程环境中使用synchronized关键字实现同步相关。
由于其实现的开销和复杂性较高,因此被称为"重量级",适合需要更严格的同步和并发控制的场景。
arduino
private synchronized void oneLock() {
//doSomething();
}
两个线程t1和t2正在同时访问该oneLock()方法。如果t1先获取锁并执行其中的同步代码块,并且 t2 也尝试访问oneLock() 方法,则它将被阻止,因为锁由t1 持有。
在这种情况下,锁处于称为重量级锁的状态。
从上面的例子可以看出,t2由于无法获取锁,因此被挂起,等待t1释放锁后再被唤醒。
线程的挂起和唤醒涉及CPU内的上下文切换,这会产生很大的开销。
由于这个过程的成本相对较高,具有这种行为的锁被称为重量级锁。
什么是轻量级锁
轻量级锁是一种同步机制,旨在减轻与传统重量级锁(例如 Java synchronized关键字提供的锁)相关的性能开销。
继续前面的示例,让我们现在考虑t1和t2交替执行oneLock()方法。
在这种情况下,t1和t2不需要阻塞,因为它们之间没有争用。换句话说,不需要重量级的锁。
当线程交替执行临界区而不发生争用时,这种场景下使用的锁被称为轻量级锁。
轻量级锁相对于重量级锁的优点:
1、每次加锁只需要一次CAS操作。
- 无需分配ObjectMonitor对象。
3、线程不需要被挂起或唤醒。
什么是偏向锁?
在只有一个线程(假设 t1)一致执行oneLock()方法的情况下,使用轻量级锁t1在每次获取锁时执行 CAS 操作。这可能会导致一些性能开销。
于是,偏向锁的概念就出现了。
当锁偏向特定线程时,该线程可以再次获取锁,而无需进行 CAS 操作。相反,简单的比较就足以获得锁。这个过程非常高效。
偏向锁相比轻量级锁的优点:
- 当同一个线程多次获取锁时,不需要再次执行 CAS 操作。简单的比较就足够了。
怎样加锁?
让我们从源代码的角度深入研究一下 Java 中这些锁是如何实现的。
锁的本质在于共享变量,所以问题的关键是如何访问这些共享变量。了解这一点就了解了这三种锁的演变过程的一半。
接下来我将从源码分析的角度重点介绍一下这些信息。
既然我们处理的是锁,自然就涉及到锁的获取和释放操作,而在偏向锁的情况下,还有锁撤销操作。
对象头是Java对象在内存中布局的一部分,用于存储对象的元数据信息和锁定状态。
在深入源码之前,我们先推测一下线程 t1 获取偏向锁的过程:
- 首先检查Mark Word中的线程ID是否有值。
- 如果没有,则意味着还没有线程获得锁。本例中,直接将t1的线程ID记录到Mark Word中。多个线程可能会尝试同时修改Mark Word,因此需要CAS操作来修改Mark Word。
- 如果已经有一个 ID 值,那么有两种可能性:
- 如果该ID是t1的ID,那么本次锁获取就是一个可重入的过程,t1可以直接获取锁。
- 如果该ID不是t1的ID,则意味着另一个线程已经获取了锁。这种情况下,t1需要经过撤销过程来获取锁。
scss
CASE(_monitorenter): {
// 1. 获取对象头,表示为"oop"(普通对象指针)。
oop lockee = STACK_OBJECT(-1);
CHECK_NULL(lockee);
BasicObjectLock* limit = istate->monitor_base();
BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
BasicObjectLock* entry = NULL;
while (most_recent != limit ) {
// 2. 遍历线程栈找到对应的可用BasicObjectLock。
if (most_recent->obj() == NULL) entry = most_recent;
else if (most_recent->obj() == lockee) break;
most_recent++;
}
if (entry != NULL) {
// 3. BasicObjectLock 的 _obj 字段指向 oop。
entry->set_obj(lockee);
int success = false;
uintptr_t epoch_mask_in_place = (uintptr_t)markOopDesc::epoch_mask_in_place;
// 从对象头中检索标记
markOop mark = lockee->mark();
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();
// 4. 获取异或运算的结果。
anticipated_bias_locking_value =
(((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &
~((uintptr_t) markOopDesc::age_mask_in_place);
if (anticipated_bias_locking_value == 0) {
// 5. 如果相等,则认为是可重入获取锁。
if (PrintBiasedLockingStatistics) {
(* BiasedLocking::biased_lock_entry_count_addr())++;
}
success = true;
}
else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) {
// 6. 如果不支持偏向锁
markOop header = lockee->klass()->prototype_header();
if (hash != markOopDesc::no_hash) {
header = header->copy_set_hash(hash);
}
// 执行CAS操作,将Mark Word修改为解锁状态。
if (Atomic::cmpxchg_ptr(header, lockee->mark_addr(), mark) == mark) {
if (PrintBiasedLockingStatistics)
(*BiasedLocking::revoked_lock_entry_count_addr())++;
}
}
else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {
// 7. 如果epoch已过期,则使用当前线程的ID构造偏向锁
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);
}
if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), mark) == mark) {
if (PrintBiasedLockingStatistics)
(* BiasedLocking::rebiased_lock_entry_count_addr())++;
} else {
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
success = true;
}
else {
// 8. 构造一个匿名偏向锁。
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);
DEBUG_ONLY(entry->lock()->set_displaced_header((markOop) (uintptr_t) 0xdeaddead);)
// 执行CAS操作将锁修改为与当前线程关联的偏向锁。
if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) {
if (PrintBiasedLockingStatistics)
(* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;
}
else {
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;
if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
entry->lock()->set_displaced_header(NULL);
} else {
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
}
}
UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
} else {
istate->set_msg(more_monitors);
UPDATE_PC_AND_RETURN(0); // Re-execute
}
}
代码比较多,下面我将对代码注释中注释1-8标注的内容进行详细解释。
# 1.oop代表对象头,包含Mark Word和Klass Word。
# 2.BasicObjectLock的结构如下:
kotlin
#basicLock.hpp
class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
friend class VMStructs;
private:
BasicLock _lock;
oop _obj;
...
};
class BasicLock VALUE_OBJ_CLASS_SPEC {
friend class VMStructs;
private:
volatile markOop _displaced_header;
...
};
BasicObjectLock是著名的Lock Record的实现,它包括两个元素:
- 存储Mark Word的移位头_displaced_header。
- 指向对象头的指针:_obj。
# 3、将Lock Record中的_obj字段赋值给lockee,代表对象头。
# 4. 从对象头lockee中,检索Klass Word,它是指向Klass类型的指针。在Klass类内部,有一个名为_prototype_header的字段,它也代表Mark Word。它存储偏向锁定标志之类的信息。
在此步骤中,提取此信息并将其与当前线程 ID 连接起来。
然后与对象头中的Mark Word 执行XOR 运算。目标是识别不同的位。
后续步骤涉及确定Mark Word的哪些特定部分不相等,从而导致不同的处理逻辑。
# 5. 如果上面的异或运算结果相等,则表明Mark Word中包含当前线程ID,并且epoch和偏向锁标志一致。
这表明该锁已经被当前线程持有,表明是可重入的。由于线程已经拥有锁,因此不需要采取进一步的操作。
# 6. 观察Mark Word中的偏向锁标志与Klass中的偏向锁标志不一致,并且考虑到Mark Word已经被识别为具有偏向锁,因此可以推断Klass不再支持偏向锁。
鉴于不支持偏向锁定,标记字被修改以反映解锁状态。这为进一步升级到轻量级锁定或重量级锁定做好了准备。
# 7. 在识别出 Mark Word 中的纪元与 Klass 中的标记之间的差异后,可以推断发生了批量重新偏置。这种情况下,直接修改Mark Word,使其偏向当前线程。
# 8、如果以上条件都不满足,则表明是匿名偏向锁(不偏向任何线程的偏向锁)。在这种情况下,会尝试直接修改Mark Word以偏向当前线程
总结
- 每次线程尝试获取锁时,都需要关联一个锁记录,并将"_obj"指针设置为对象头。这在锁定记录和对象头之间建立了连接。
- 一旦线程成功将自己的线程ID写入Mark Word,就表明该线程已经获得了偏向锁。
在偏向锁状态下,锁记录和对象头之间建立了关系。这种关系由指向对象头的锁定记录的 _obj 字段表示。
我们回顾一下线程t1和t2获取偏向锁的过程:
- 线程 t1 尝试获取锁。最初,锁处于匿名偏向状态, T1成功获取锁。
- 线程 t1 尝试再次获取锁。由于它已经持有锁,所以他会来获取可重入锁。
- 同时,线程 t2 尝试获取锁。由于 t1 当前持有锁定,因此 t2 会锁撤销。
锁撤销
如果尝试获取偏向锁不成功,锁将恢复为未锁定状态,然后升级为轻量级锁。此过程称为偏向锁撤销。
php
#InterpreterRuntime.cpp
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
...
if (UseBiasedLocking) {
// 当使用偏向锁时,进程进入快速路径执行。
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
// 升级为轻量级锁。
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
...
#synchronizer.cpp
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) {
return;
}
} else {
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 (obj, lock, THREAD) ;
}
可见,撤销分为安全点撤销和非安全点撤销。
非安全点撤销,也称为"revoke_and_rebias",发生在未等待安全点而撤销偏向锁时。在这个过程中,偏向锁被直接撤销,并且对象的标记字被更新以反映新的状态,而不需要安全点来保证一致的状态转换。
当发生非安全点撤销时,偏向锁的状态从偏向变为正常或可重偏向。
如果它更改为可重偏向状态,则意味着如果另一个线程寻求该锁,该锁可以再次偏向。
这允许更快、更有效的锁定转换,因为如果另一个线程在撤销后不久获取该锁,则该锁可能会跳过中间状态并直接进入偏向状态。
从本质上讲,非安全点撤销减少了等待安全点的需要,并实现了更灵活、响应更灵敏的方法来撤销偏向锁,从而提高了性能并减少了某些场景下的锁争用。
批量重新偏向和批量撤销
经过以上分析,我们了解到以下几点:
- 当一个线程持有偏向锁,而另一个线程试图获取该锁时,需要撤销该锁。
- 撤销过程首先尝试使用CAS在非安全点将Mark Word更改为解锁状态。如果仍然无法实现撤销,则可以考虑在安全点执行撤销的选项,尽管在安全点执行撤销的效率相对较低。
因此,偏向锁引入了批量重偏向和批量撤销的概念。
当对象的锁被撤销的次数达到一定阈值时,例如20次,就会触发批量重偏逻辑。
这涉及到修改 Klass 中的标记以及当前使用的该类型锁的 Mark Word 中的标记。
当线程尝试获取偏向锁时,它会将当前对象的纪元值与 Klass 中的标记值进行比较。
如果不相等,则认为锁已过期。在这种情况下,允许线程直接CAS修改Mark Word以偏向当前线程,避免撤销逻辑。这对应于偏向锁进入最初讨论中的分析标签(7)。
同样,当撤销次数达到40次时,就认为该对象不再适合偏向锁。
因此,Klass 中的偏向锁标志发生更改,以指示不再支持偏向锁。
当线程尝试获取偏向锁时,它会检查 Klass 中的偏向锁标志。如果不再允许偏差,则表明批次撤销较早发生。
在这种情况下,允许线程直接CAS将Mark Word修改为解锁状态,避免了撤销逻辑。这对应于偏向锁进入最初讨论中的分析标签(6)。
批量重新偏向和批量撤销是旨在提高偏向锁定性能的优化。
锁释放
scss
#bytecodeInterpreter.cpp
CASE(_monitorexit): {
oop lockee = STACK_OBJECT(-1);
CHECK_NULL(lockee);
BasicObjectLock* limit = istate->monitor_base();
BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
// 遍历线程栈
while (most_recent != limit ) {
// 查找对应的锁记录
if ((most_recent)->obj() == lockee) {
BasicLock* lock = most_recent->lock();
markOop header = lock->displaced_header();
// 将锁定记录中的_obj字段设置为null
most_recent->set_obj(NULL);
// 这是轻量级锁的释放。
UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
}
most_recent++;
}
...
}
您可能已经注意到,Mark Word 没有改变;它仍然偏向于前一个线程。然而,锁还没有被释放。事实上,当线程退出临界区时,它不会释放偏向锁。
原因是:
当再次需要锁时,简单的按位比较就可以快速判断是否是可重入获取。这意味着不需要每次都执行CAS操作就可以高效地获取锁。这种效率是偏向锁在只有一个线程访问锁的场景下的核心优势。
总结
- 偏向锁中的"锁"指的是Mark Word。修改Mark Word是获取锁所必需的,由于潜在的多线程争用,这可能会涉及CAS操作。
- 由于撤销操作在安全点执行时效率可能较低,并且多次撤销会进一步影响效率,因此引入了批量重偏和撤销机制。
- 偏向锁的可重入计数取决于线程堆栈中存在的锁记录的数量。
- 如果偏向锁撤销失败,锁最终会升级为轻量级锁。
- 退出时,偏向锁不会修改Mark Word,也就是说锁没有被释放。
未完待续。。。。。
如果喜欢这篇文章,点赞支持一下,微信搜索:京城小人物,关注我第一时间查看更多内容!感谢支持!