揭秘Java synchronize :管程ObjectMonitor
本文深入解析了Java对象监视器ObjectMonitor的核心机制。首先介绍了ObjectMonitor的角色和来源,指出它是在锁膨胀为重量级锁时由JVM分配的。随后详细分析了ObjectMonitor的数据结构,包括_owner、_recursions、_entry_list等关键字段的作用。重点阐述了ObjectMonitor的主要工作流程:enter操作通过自旋和排队机制处理锁获取,exi
ObjectMonitor
首先,我们要明白 ObjectMonitor
是什么,它在何时出现。
- 角色:
ObjectMonitor
是实现 Java 对象监视器(Monitor)的底层结构。每个 Java 对象都可以逻辑上关联一个监视器。当多个线程竞争同一个对象的锁时,这个监视器负责管理线程的排队、阻塞和唤醒。 - 来源(锁膨胀):一个 Java 对象最初并没有
ObjectMonitor
。它的对象头(markWord
)中记录的是锁状态。当线程竞争加剧,锁会从偏向锁 -> 轻量级锁 -> 重量级锁进行“膨胀”。只有当锁膨胀为重量级锁时,JVM 才会从一个全局列表中分配一个ObjectMonitor
实例,并让对象的markWord
指向这个ObjectMonitor
。
ObjectMonitor
的设计目标是健壮性和公平性,而不是极致的性能(那是偏向锁和轻量级锁的目标)。
ObjectMonitor
的核心数据结构
让我们从 ObjectMonitor
的构造函数和其成员变量定义入手,理解它的内部状态。
// ... existing code ...
ObjectMonitor::ObjectMonitor(oop object) :
_metadata(0),
_object(_oop_storage, object),
_owner(NO_OWNER),
_previous_owner_tid(0),
_next_om(nullptr),
_recursions(0),
_entry_list(nullptr),
_entry_list_tail(nullptr),
_succ(NO_OWNER),
_SpinDuration(ObjectMonitor::Knob_SpinLimit),
_contentions(0),
_wait_set(nullptr),
_waiters(0),
_wait_set_lock(0),
_stack_locker(nullptr)
{ }
// ... existing code ...
这里每一个字段都至关重要,我们来逐一解析:
_object
: 一个OopHandle
,指向与此ObjectMonitor
关联的 Java 对象实例。这是连接 C++ 世界和 Java 世界的桥梁。_owner
: 最核心的字段之一。它记录了当前持有该监视器的线程。它的值通常是线程的唯一标识。如果为NO_OWNER
(通常是nullptr
),表示锁当前未被任何线程持有。_recursions
: 记录锁的重入次数。如果一个线程已经持有了锁,再次进入synchronized
块时,_recursions
会递增。退出时递减。当_recursions
减到 0 时,线程才会真正释放锁。_entry_list
: 核心字段。一个指针,指向一个等待获取锁的线程队列的头部。当一个线程尝试获取锁失败时,它会被封装成一个节点并加入到这个队列中,然后被挂起(park)。这是一个单向链表,但在某些情况下会临时构造成双向链表以优化性能。_entry_list_tail
:_entry_list
的尾指针。用于实现 FIFO(先进先出)的公平策略,让等待最久的线程优先获得锁。_wait_set
: 核心字段。一个指针,指向调用了该对象Object.wait()
方法的线程队列。这些线程已经放弃了锁,并等待被notify()
或notifyAll()
唤醒。_succ
: 全称 "Successor"(继承者)。当一个线程释放锁时,它会从_entry_list
中挑选一个线程作为“继承者”并唤醒它。_succ
记录了这个被选中的线程。被唤醒的线程并不直接拥有锁,而是获得优先竞争权。这种策略被称为“竞争性交接”(Competitive Handoff)。_contentions
: 一个计数器,记录该锁历史上发生竞争的次数。_SpinDuration
: 控制自旋等待的时间。在进入阻塞状态之前,线程会尝试自旋一小段时间,看能否获得锁,以避免线程挂起和恢复的巨大开销。
核心工作流程分析
ObjectMonitor
的主要工作流程围绕 enter
、exit
、wait
、notify
四个核心操作展开。
A. enter
(获取锁)
这是最复杂的操作,我们分步来看 ObjectMonitor::enter
的过程。
// ... existing code ...
bool ObjectMonitor::enter(JavaThread* current) {
assert(current == JavaThread::current(), "must be");
if (spin_enter(current)) {
return true;
}
// ... existing code ...
// Keep is_being_async_deflated stable across the rest of enter
ObjectMonitorContentionMark contention_mark(this);
// Check for deflation.
if (enter_is_async_deflating()) {
return false;
}
// At this point this ObjectMonitor cannot be deflated, finish contended enter
enter_with_contention_mark(current, contention_mark);
return true;
}
// ... existing code ...
- 尝试自旋 (
spin_enter
):首先,代码会尝试通过自旋快速获取锁。如果_owner
是NO_OWNER
,就通过 CAS(Compare-And-Swap)原子操作将_owner
设置为当前线程。如果成功,直接返回。如果_owner
就是当前线程自己,就增加_recursions
计数,实现锁重入。 - 进入慢速路径(Contended Enter):如果自旋失败,说明存在真正的竞争。此时进入
enter_with_contention_mark
。 - 处理虚拟线程(Project Loom):
这是对虚拟线程的特殊处理。平台线程阻塞会占用一个宝贵的 OS 线程资源。而虚拟线程在阻塞时,会尝试 卸载(unmount) 自己,即将 Java 栈保存到堆上,然后让其载体平台线程(Carrier Thread)去执行其他任务。// ... existing code ... ContinuationEntry* ce = current->last_continuation(); bool is_virtual = ce != nullptr && ce->is_virtual_thread(); if (is_virtual) { notify_contended_enter(current); result = Continuation::try_preempt(current, ce->cont_oop(current)); if (result == freeze_ok) { bool acquired = vthread_monitor_enter(current); // ... existing code ...
Continuation::try_preempt
就是这个卸载操作的开始。 - 入队与阻塞(平台线程或虚拟线程卸载失败):
- 线程被封装成一个
ObjectWaiter
节点。 - 通过 CAS 操作,将这个节点“压”入
_entry_list
的头部。这是一个无锁操作,可以支持高并发的入队请求。 - 调用
park()
方法(底层是PlatformEvent::park
或Parker::park
),将当前线程挂起,等待被唤醒。
- 线程被封装成一个
B. exit
(释放锁)
exit
的逻辑相对简单,但包含关键的唤醒策略。
- 处理重入:如果
_recursions > 0
,则只递减_recursions
计数,不释放锁。 - 释放锁:当
_recursions
为 0 时,将_owner
设置为NO_OWNER
。 - 唤醒继承者:这是
exit
的关键。- 它会检查
_entry_list
是否为空。 - 如果不为空,它会从队列中挑选一个线程作为继承者(通常是队尾的线程,实现 FIFO)。
- 这个挑选过程非常精妙,如文件顶部的注释所述,如果
_entry_list_tail
未知,它会遍历_entry_list
这个单向链表,并在遍历过程中设置prev
指针,将其临时变成一个双向链表,从而高效地找到队尾。 - 找到继承者后,将其记录在
_succ
字段,并调用unpark()
唤醒它。
- 它会检查
C. wait
& notify
这两个操作与 _wait_set
队列紧密相关。
-
wait()
:- 将当前线程加入到
_wait_set
队列中。 - 完全释放锁(包括所有重入),将
_owner
设为NO_OWNER
。 - 唤醒一个
_entry_list
中的线程,让它来竞争锁。 - 调用
park()
挂起当前线程,等待notify
。 - 被唤醒后,线程会从
_wait_set
移到_entry_list
中,重新开始竞争锁。
- 将当前线程加入到
-
notify()
/notifyAll()
:notify()
从_wait_set
中取出一个线程(通常是等待最久的)。notifyAll()
取出_wait_set
中所有的线程。- 关键:这些被取出的线程并不会被立即唤醒,而是被移动到
_entry_list
队列的尾部。 - 为什么不直接唤醒?因为
notify
的调用者仍然持有锁。如果直接唤醒等待线程,它会立刻尝试获取锁,但必然失败并再次阻塞,这被称为“无用的唤醒”(futile wakeup)。将其移入_entry_list
,等notify
调用者执行完exit
释放锁后,再由exit
的逻辑来唤醒,这样就保证了公平性和效率。
总结:ObjectMonitor
的设计哲学
ObjectMonitor
是一个精巧而复杂的 C++ 实现,体现了以下设计哲学:
- 混合使用无锁与有锁:在线程争抢进入
_entry_list
时,使用无锁的 CAS 操作来提高并发性。而在队列内部操作(如出队)时,则依赖于持有ObjectMonitor
锁的线程来保证安全,避免了更复杂的无锁算法。 - 公平性策略:通过
_entry_list
和_wait_set
两个队列,并采用 FIFO 策略,尽可能保证线程饥饿问题不会发生。 - 性能优化:通过自旋、将
notify
的线程移入entry_list
而非直接唤醒等策略,减少不必要的线程上下文切换和系统调用。 - 面向未来的设计:对虚拟线程的集成支持,表明了其设计在不断演进,以适应 JVM 的最新发展(如 Project Loom)。
- 健壮性优先:相比
Parker
的简洁,ObjectMonitor
及其关联的ParkEvent
使用了更复杂的机制(如三态状态机、不朽对象池),根本目的是为了在synchronized
这种基础、复杂的场景下保证绝对的线程安全和状态一致性。
ObjectMonitor队列设计的必要性分析
ObjectMonitor 维护队列(如 EntryList、WaitSet)而非直接使用 mutex 的 lock/wait/signal,主要原因如下:
-
支持 Java 层语义(wait/notify)
-
Java 的
Object.wait()
/notify()
/notifyAll()
要求线程有序进入等待和唤醒状态。 -
wait()
需将线程放入等待队列(WaitSet),等待被notify()
或notifyAll()
唤醒。 -
notify()
仅唤醒一个等待线程,notifyAll()
唤醒所有等待线程。 -
仅用 mutex 的
lock/wait/signal
无法区分:-
正在竞争锁的线程
-
调用
wait()
进入等待的线程
-
-
无法实现 Java 的精确唤醒语义。
-
-
支持公平性与高效性
-
队列可保证线程唤醒的顺序性(如 FIFO)和公平性。
-
直接使用 mutex 时:
-
线程唤醒顺序不可控
-
易引发“惊群效应”(thundering herd),降低性能。
-
-
-
支持多状态管理
-
ObjectMonitor 需区分以下线程状态:
-
EntryList:正在竞争锁的线程
-
WaitSet:已调用
wait()
的线程 -
Owner(支持可重入):当前持有锁的线程
-
-
队列是实现这些状态管理的必要机制,以正确支持
synchronized
和wait/notify
的复杂语义。
-
-
互斥锁的 wait/notify 语义局限
-
互斥锁的
wait/notify
仅适用于简单的生产者-消费者模型。 -
ObjectMonitor 需支持更复杂的线程调度和唤醒策略,单一 mutex 无法满足需求。
-
虚拟线程支持
从源码可以看到大量虚拟线程特殊处理:
bool ObjectMonitor::vthread_monitor_enter(JavaThread* current, ObjectWaiter* waiter) {
// 虚拟线程需要特殊的挂载/卸载逻辑
java_lang_VirtualThread::set_state(vthread, java_lang_VirtualThread::BLOCKING);
}
- 虚拟线程可能需要卸载(unmount)而不是阻塞
- 需要维护复杂的状态转换
- 简单mutex无法支持这种异步操作模式
总结
ObjectMonitor的队列设计是为了:
- 弥合Java同步语义与操作系统原语之间的语义鸿沟
- 提供高性能的用户态同步优化
- 支持现代JVM特性(如虚拟线程)
- 实现精确的线程状态管理和公平性控制
简单的mutex lock/wait/signal无法满足Java Monitor的复杂语义要求,队列机制是实现这些高级特性的必要基础设施。
ObjectMonitor 与 CLH、AQS
ObjectMonitor 的实现与 CLH(Craig, Landin, and Hagersten 队列锁)和 AQS(AbstractQueuedSynchronizer,Java并发包的核心同步器)有相似之处,但也有明显不同。下面简要分析:
相似点
-
队列管理等待线程
- ObjectMonitor 通过 _entry_list(进入队列)和 _wait_set(等待队列)管理等待锁和等待条件的线程。
- CLH/AQS 都是通过链表(队列)管理等待线程,AQS 也是通过一个双向链表(CLH 是隐式队列,AQS 是显式队列)来管理。
-
公平性和唤醒策略
- ObjectMonitor 选择队列尾部的线程作为"继任者"唤醒,保证一定的公平性。
- AQS 也是通过队列唤醒下一个节点,CLH 也是类似的"排队"思想。
-
自旋与阻塞结合
- ObjectMonitor 先尝试自旋获取锁,失败后才进入阻塞队列。
- AQS/CLH 也有类似的自旋-阻塞混合策略(AQS 支持自旋,CLH 本身就是自旋锁)。
-
CAS 操作
- 都大量使用 CAS(Compare-And-Swap)来保证队列和锁状态的原子性。
不同点
-
队列结构
- ObjectMonitor 的 _entry_list 是单链表,必要时转为双链表,且队列头尾指针管理较为复杂。
- AQS 是显式的双向链表,节点结构更清晰,且有头尾指针。
- CLH 是隐式队列,每个线程持有自己的节点和前驱节点的引用。
-
锁的传递方式
- ObjectMonitor 并不直接"传递"锁,而是采用竞争式唤醒(竞争式 handoff),即唤醒后线程还需重新竞争锁。
- AQS和Object Monitor类似。AQS 唤醒线程后,线程还需要重新竞争锁(不是直接获得锁)。AQS 通过条件队列(Condition Queue)机制,实现了类似 ObjectMonitor 的 wait/notify/notifyAll 功能。
- CLH 是严格的 FIFO,自旋在前驱节点上,前驱释放后自己获得锁。
-
虚拟线程支持
- ObjectMonitor 针对虚拟线程(vthread)做了大量特殊处理(如挂起、解挂、特殊的唤醒机制),AQS/CLH 没有这部分内容。
-
wait/notify 支持
- ObjectMonitor 原生支持 Java 的 wait/notify/notifyAll 语义,AQS 需要配合 ConditionObject 实现,CLH 本身不直接支持条件变量。
-
实现复杂度
- ObjectMonitor 兼容了多种线程模型、GC、JVM 事件、JVMTI、JFR 等,代码复杂度远高于 AQS/CLH。
总结
- ObjectMonitor 的队列思想和唤醒策略与 AQS/CLH 有很多相似之处,都是"排队-唤醒-竞争"模式。
- 但 ObjectMonitor 由于 JVM 层的特殊需求(如虚拟线程、GC、JVMTI、wait/notify 语义等),实现上比 AQS/CLH 更复杂,且有很多专门的优化和特殊处理。
- AQS 更适合 Java 用户态的同步器开发,ObjectMonitor 是 JVM 内部的重量级同步原语。
ObjectMonitor 的实现思想与 CLH、AQS 有相似之处,但实现细节和复杂度远高于它们,且有大量 JVM 特有的处理。可以认为它是"类 CLH/AQS 队列锁 + JVM 特性扩展"的产物。
ObjectMonitor::enter
ObjectMonitor::enter
是 monitorenter
字节码的最终 C++ 实现。当一个线程尝试进入一个 synchronized
代码块,并且该对象的锁已经膨胀为重量级锁时,就会调用这个函数。
它的设计遵循一个分层优化的策略:
- 快速路径 (Fast Path):尝试通过自旋 (Spinning) 快速获取锁,避免线程阻塞带来的巨大开销。
- 慢速路径 (Slow Path):如果自旋失败,说明存在真正的锁竞争。此时,线程将进入一个复杂的流程,最终可能会被挂起(park),加入等待队列。
- 健壮性与协同:整个过程必须处理好与 GC、锁膨胀/锁降级(Deflation)、线程挂起(Suspend)以及虚拟线程(Project Loom)的复杂交互。
现在,我们来逐行分析代码的执行流程。
详细代码流程分析
// ... existing code ...
bool ObjectMonitor::enter(JavaThread* current) {
assert(current == JavaThread::current(), "must be");
if (spin_enter(current)) {
return true;
}
// ... existing code ...
第一步:快速自旋尝试 (spin_enter
)
函数一进来,首先调用 spin_enter(current)
,这是第一道防线,希望能快速解决战斗。
spin_enter
内部主要做几件事:
- 检查重入 (
try_enter
):首先判断当前线程是否已经是锁的持有者 (has_owner(current)
)。如果是,就简单地将_recursions
字段加一,然后直接返回true
。这是synchronized
可重入特性的实现。 - 尝试获取锁 (
try_lock
):如果锁没有持有者 (_owner == NO_OWNER
),它会通过 CAS (Compare-And-Swap) 原子操作,尝试将_owner
设置为当前线程。如果成功,获取锁,返回true
。 - 检查锁降级冲突:检查
ObjectMonitor
是否正在被异步降级 (enter_is_async_deflating
)。如果是,就放弃本次操作,返回false
,让上层重试。 - 自旋 (
try_spin
):如果锁被其他线程持有,spin_enter
会执行一小段时间的“自旋”。它不会立即放弃,而是在一个循环里空转,不断尝试获取锁。如果在这段短暂的时间内,锁被释放了,当前线程就能成功获取,从而避免了线程挂起和上下文切换的巨大成本。
如果 spin_enter
返回 true
,意味着锁已成功获取,enter
函数直接返回 true
,整个过程非常高效。
第二步:进入慢速路径 - contention_mark
如果 spin_enter
返回 false
,说明遇到了真正的竞争,锁在短时间内无法获得。代码进入慢速路径。
// ... existing code ...
assert(!has_owner(current), "invariant");
assert(!has_successor(current), "invariant");
assert(!SafepointSynchronize::is_at_safepoint(), "invariant");
assert(current->thread_state() != _thread_blocked, "invariant");
// Keep is_being_async_deflated stable across the rest of enter
ObjectMonitorContentionMark contention_mark(this);
// Check for deflation.
if (enter_is_async_deflating()) {
return false;
}
// At this point this ObjectMonitor cannot be deflated, finish contended enter
enter_with_contention_mark(current, contention_mark);
return true;
}
-
ObjectMonitorContentionMark contention_mark(this);
- 这是一个非常重要的 RAII (Resource Acquisition Is Initialization) 对象。
- 在它的构造函数中,会原子地将
_contentions
字段加一。 - 这个字段的作用是向 VM 的其他部分(特别是锁降级线程)声明:“这个 Monitor 正有线程在激烈竞争,请不要对它进行降级操作!”
- 当
contention_mark
对象离开作用域时,它的析构函数会自动将_contentions
减一。
-
再次检查锁降级 (
enter_is_async_deflating
)- 在标记了
_contentions
之后,再次进行检查。这次检查更加可靠,因为它与降级线程之间有了一个明确的“协议”。如果此时发现仍在降级,就返回false
,让上层代码(在ObjectSynchronizer::slow_enter
中)重试整个enter
过程。
- 在标记了
-
enter_with_contention_mark(...)
- 如果一切正常,就调用这个函数,进入真正的阻塞流程。
核心阻塞逻辑 (enter_with_contention_mark
)
这是 enter
函数的核心,处理线程的排队和阻塞。
// ... existing code ...
void ObjectMonitor::enter_with_contention_mark(JavaThread* current, ObjectMonitorContentionMark &cm) {
// ... existing code ...
ContinuationEntry* ce = current->last_continuation();
bool is_virtual = ce != nullptr && ce->is_virtual_thread();
if (is_virtual) {
notify_contended_enter(current);
result = Continuation::try_preempt(current, ce->cont_oop(current));
if (result == freeze_ok) {
bool acquired = vthread_monitor_enter(current);
// ... existing code ...
return;
}
}
{
// Change java thread status to indicate blocked on monitor enter.
JavaThreadBlockedOnMonitorEnterState jtbmes(current, this);
if (!is_virtual) { // already notified contended_enter for virtual
notify_contended_enter(current);
}
// ... existing code ...
for (;;) {
ExitOnSuspend eos(this);
{
ThreadBlockInVMPreprocess<ExitOnSuspend> tbivs(current, eos, true /* allow_suspend */);
enter_internal(current);
// ... existing code ...
}
if (!eos.exited()) {
// ExitOnSuspend did not exit the OM
assert(has_owner(current), "invariant");
break;
}
}
// ... existing code ...
}
// ... existing code ...
}
此函数分为两大分支:虚拟线程 和 平台线程。
A. 虚拟线程 (Virtual Thread) 分支
这是为支持 Project Loom 而新增的复杂逻辑。
if (is_virtual)
:检查当前线程是否为虚拟线程。Continuation::try_preempt(...)
:这是关键。它尝试抢占 (preempt) 并卸载 (unmount) 当前的虚拟线程。- 卸载:将虚拟线程的 Java 调用栈数据从其载体平台线程(Carrier Thread)的栈上复制到 Java 堆中。
- 释放:载体平台线程被释放,可以去执行其他的任务。
- 这样,虚拟线程的“阻塞”就不会真正占用一个宝贵的操作系统线程,极大地提高了系统的吞吐量。
vthread_monitor_enter(current)
:虚拟线程被成功卸载后,这个函数负责将其加入到_entry_list
等待队列中。- 竞争与取消:如果在尝试卸载的过程中,锁恰好被释放了,
vthread_monitor_enter
可能会直接获取到锁。此时,它会设置一个“取消抢占”的标志,阻止卸载的发生,让虚拟线程继续在原平台线程上执行。
B. 平台线程 (Platform Thread) 分支
这是传统的线程阻塞路径。
JavaThreadBlockedOnMonitorEnterState jtbmes(...)
:又一个 RAII 对象,它负责将线程的状态设置为_thread_blocked_on_monitor_enter
。这样,当使用jstack
等工具查看线程堆栈时,就能明确看到该线程正在等待进入一个监视器。for (;;)
循环:这是一个无限循环,但通常只会执行一次。它的主要目的是为了安全地处理线程挂起 (suspend) 请求。ExitOnSuspend eos(this)
:定义一个回调对象。ThreadBlockInVMPreprocess<ExitOnSuspend> tbivs(...)
:这是一个关键的 VM 操作。它在准备阻塞线程前,会检查是否有外部请求(如 JVMTI agent)要挂起当前线程。- 如果有挂起请求,
eos
对象的回调函数会被触发。这个回调会立即释放掉已经获取的锁(如果恰好获取了),并设置一个“待定监视器”标志,然后线程会安全地进入挂起状态。这避免了线程在持有锁的情况下被挂起而导致的死锁。 enter_internal(current)
:如果没有挂起请求,则调用此函数。- 它会在当前线程的栈上创建一个
ObjectWaiter
节点。 - 通过 CAS 操作将此节点压入
_entry_list
队列的头部。 - 调用
park()
方法,将当前线程挂起,等待被唤醒。
- 它会在当前线程的栈上创建一个
- 当
enter_internal
返回时,意味着线程已被唤醒,并且已经成为了锁的持有者。 if (!eos.exited()) { break; }
:检查eos
是否因为挂起而退出了监视器。如果没有,说明正常获取了锁,跳出循环。
成功获取锁后的收尾工作
当线程从 for(;;)
循环中出来后,它已经成功持有了锁。
- 状态断言:一系列
assert
语句确保_recursions
为 0,_owner
是当前线程等状态的正确性。 - 事件通知:调用
DTRACE_MONITOR_PROBE
和JvmtiExport::post_monitor_contended_entered
等函数,发布 JFR、DTrace、JVMTI 事件。这使得外部工具可以观测到锁竞争和获取的详细情况。 - 虚拟线程固定事件 (Pinned Event):如果一个虚拟线程因为某些原因未能成功卸载,而是像平台线程一样阻塞了其载体线程,这里会发布一个“线程被固定 (pinned)”的事件。这对诊断虚拟线程的性能问题至关重要。
总结
ObjectMonitor::enter
是一个集性能优化、多场景健壮性处理和未来技术(虚拟线程)支持于一体的典范。其逻辑可以概括为:
- 乐观尝试:通过自旋,用最小的代价获取锁。
- 悲观准备:进入慢速路径,通过
_contentions
标记来防止降级冲突。 - 分类处理:为虚拟线程提供高效的“卸载”机制,为平台线程提供安全的“挂起”机制。
- 安全第一:通过 RAII 和严谨的状态检查,确保在线程挂起、锁降级等复杂并发场景下的数据一致性和无死锁。
- 高度可观测:深度集成 JFR、DTrace、JVMTI 等监控工具。
ObjectMonitor::enter_internal
当一个线程经过了快速路径(spin_enter
)的多次尝试仍然无法获取锁之后,就会进入这个函数,准备进行排队和阻塞。它被 enter_with_contention_mark
调用,此时线程状态已经被标记为 _thread_blocked
。
enter_internal
的策略可以概括为“最后的挣扎,然后体面地排队睡觉”。
- 最后的挣扎:在将线程加入等待队列(
_entry_list
)之前,再进行最后几轮的try_lock
和try_spin
。因为入队和出队是有开销的,能避免则尽量避免。 - 体面地排队:如果实在获取不到锁,就将自己封装成一个
ObjectWaiter
节点,通过一个高度优化的try_lock_or_add_to_entry_list
操作加入队列。 - 安全地睡觉:进入一个循环,调用
park()
方法挂起线程。这个循环必须处理好各种唤醒情况(正常唤醒、伪唤醒、虚拟线程超时唤醒)并保证最终能安全地获取锁。 - 干净地离场:成功获取锁后,将自己的节点从队列中移除,并清理状态。
详细代码流程分析
void ObjectMonitor::enter_internal(JavaThread* current) {
assert(current->thread_state() == _thread_blocked, "invariant");
// Try the lock - TATAS
if (try_lock(current) == TryLockResult::Success) {
assert(!has_successor(current), "invariant");
assert(has_owner(current), "invariant");
return;
}
// ... existing code ...
第一阶段:入队前的最后尝试
这是进入阻塞流程前的最后机会。
-
if (try_lock(current) == TryLockResult::Success)
- 这是经典的“Test-And-Test-And-Set” (TATAS) 优化中的一环。在进入更昂贵的自旋之前,先简单地测试一下锁是否可用。如果恰好锁被释放了,就能立即获取并返回,避免了后续所有开销。
-
if (try_spin(current))
- 如果
try_lock
失败,再进行一轮自旋。这给了线程一个在 CPU 上空转一小段时间的机会,等待锁的释放。这对于锁持有时间很短的场景非常有效。如果自旋成功,同样直接返回。
- 如果
第二阶段:入队与最后的竞争
如果自旋也失败了,说明锁的竞争比较激烈,必须准备排队了。
// ... existing code ...
// The Spin failed -- Enqueue and park the thread ...
assert(!has_successor(current), "invariant");
assert(!has_owner(current), "invariant");
ObjectWaiter node(current);
current->_ParkEvent->reset();
if (try_lock_or_add_to_entry_list(current, &node)) {
return; // We got the lock.
}
// This thread is now added to the _entry_list.
// ... existing code ...
-
ObjectWaiter node(current);
- 在当前线程的栈上创建一个
ObjectWaiter
对象。这个对象是线程在_entry_list
中的代理。栈上分配避免了堆分配的开销和管理复杂性。
- 在当前线程的栈上创建一个
-
current->_ParkEvent->reset();
- 重置与当前线程关联的
ParkEvent
。ParkEvent
是实现线程挂起/唤醒的底层机制。reset()
将其内部状态清理干净,确保park()
行为符合预期。
- 重置与当前线程关联的
-
if (try_lock_or_add_to_entry_list(current, &node))
- 这是一个高度优化的关键函数。它尝试做两件事:要么获取锁,要么把自己加入队列。
- 它内部会使用
Atomic::cmpxchg
尝试将node
原子地设置为_entry_list
的新头部。 - 如果
cmpxchg
失败,意味着在它尝试入队的同时,有其他线程修改了_entry_list
(通常是持有锁的线程在exit
时唤醒了一个等待者)。这是一个强烈的信号,说明锁可能刚刚被释放。所以,它不会立即重试入队,而是立刻再次调用try_lock
。如果这次try_lock
成功了,它就直接返回true
,避免了入队。 - 如果
cmpxchg
成功,意味着node
已经安全地加入_entry_list
的头部,函数返回false
。
至此,如果代码继续向下执行,那么当前线程的 ObjectWaiter
节点已经确定在等待队列中了。
第三阶段:循环、挂起与唤醒
这是线程生命中最漫长的一段等待。
// ... existing code ...
// For virtual threads that are pinned, do a timed-park instead to
// alleviate some deadlocks cases where the succesor is an unmounted
// virtual thread that cannot run.
static int MAX_RECHECK_INTERVAL = 1000;
int recheck_interval = 1;
bool do_timed_parked = false;
ContinuationEntry* ce = current->last_continuation();
if (ce != nullptr && ce->is_virtual_thread()) {
do_timed_parked = true;
}
for (;;) {
if (try_lock(current) == TryLockResult::Success) {
break;
}
assert(!has_owner(current), "invariant");
// park self
if (do_timed_parked) {
current->_ParkEvent->park((jlong) recheck_interval);
// Increase the recheck_interval, but clamp the value.
recheck_interval *= 8;
if (recheck_interval > MAX_RECHECK_INTERVAL) {
recheck_interval = MAX_RECHECK_INTERVAL;
}
} else {
current->_ParkEvent->park();
}
if (try_lock(current) == TryLockResult::Success) {
break;
}
// ... existing code ...
if (try_spin(current)) {
break;
}
// ... existing code ...
if (has_successor(current)) clear_successor();
// Invariant: after clearing _succ a thread *must* retry _owner before parking.
OrderAccess::fence();
}
// ... existing code ...
-
for (;;)
循环:线程将在此循环,直到成功获取锁。 -
入循环后立即
try_lock
:这是为了解决一个经典的**“信号丢失”**竞争问题。设想一下:线程 A 刚刚把自己加入队列,但还没来得及park
。此时,持有锁的线程 Bexit
,检查_entry_list
,发现了 A,于是unpark(A)
,然后走掉。如果 A 此后直接park
,它将永远等待,因为唤醒信号已经错过了。所以在park
之前必须再检查一次锁。 -
虚拟线程的特殊处理 (
do_timed_parked
):- 如果当前是被固定 (pinned) 的虚拟线程,它不能无限期地
park
,因为这会永久占用一个宝贵的平台载体线程。虚拟线程的「固定(Pinned)」场景,虚拟线程通常由JVM调度到平台线程(Carrier Thread)上执行,但某些操作(如执行native
代码或持有某些锁时)会导致虚拟线程被固定(Pinned)到当前平台线程,无法被卸载。风险:如果大量被固定的虚拟线程无限期地park()
(阻塞),它们会永久占用平台线程(操作系统线程),而平台线程的数量是有限的(通常等于CPU核心数或稍多)。 - 它会使用带超时的
park()
。第一次超时很短(1ms),然后指数级增加,直到一个上限(1000ms)。 - 目的:这是一种死锁规避机制。想象一个场景:所有平台线程都被固定住了,都在等待一个由尚未运行的虚拟线程持有的资源(例如类加载锁)。如果所有线程都无限期
park
,系统就死锁了。定期的超时唤醒给了系统一个“喘息”的机会,让调度器有机会去运行那个关键的虚拟线程。
- 如果当前是被固定 (pinned) 的虚拟线程,它不能无限期地
-
平台线程的
park()
:对于普通平台线程,直接调用current->_ParkEvent->park()
,无限期等待,直到被unpark
。 -
唤醒后的逻辑:当
park()
返回后(无论是被唤醒还是超时):- 立即
try_lock
:这是第一要务。 - 再次
try_spin
:如果try_lock
失败,说明锁仍然被别人持有(可能是伪唤醒,或者刚被别人抢走)。再自旋一次,也许能抢到。 - 清理
_succ
:如果当前线程是被前一个所有者指定为“继承者”(_succ
)而被唤醒的,但经过try_lock
和try_spin
仍然没拿到锁,那么它就失去了作为“继承者”的资格,必须调用clear_successor()
清理这个状态标记。 OrderAccess::fence()
:一个内存屏障,确保清理_succ
的操作对所有其他CPU可见,然后才能安全地再次尝试获取锁或重新进入park
。
- 立即
第四阶段:获取锁后的清理工作
当线程最终跳出 for
循环时,它已经成功持有了锁。
// ... existing code ...
// Egress :
// Current has acquired the lock -- Unlink current from the _entry_list.
unlink_after_acquire(current, &node);
if (has_successor(current)) {
clear_successor();
// Note that we don't need to do OrderAccess::fence() after clearing
// _succ here, since we own the lock.
}
// ... existing code ...
return;
}
unlink_after_acquire(current, &node)
:将当前线程的ObjectWaiter
节点从_entry_list
中安全地移除。这是一个需要小心处理链表指针的操作。if (has_successor(current)) clear_successor()
:再次检查并清理_succ
状态。因为持有锁,所以这里的操作是线程安全的,不需要内存屏障。- JMM 保证:函数末尾的大段注释解释了Java内存模型(JMM)的保证。简单来说,获取锁的操作(CAS)带有“acquire”语义,而后续释放锁的操作(
exit
)带有“release”语义。这确保了线程在持有锁期间对内存的所有修改,对于下一个获取到该锁的线程都是可见的,从而保证了synchronized
的内存可见性。
总结
ObjectMonitor::enter_internal
是一个集多种优化与健壮性设计于一体的函数。它通过多轮次的自旋和尝试来避免阻塞,通过原子操作和精巧的算法来安全地管理等待队列,通过对虚拟线程的特殊处理来适应未来的并发模型,并通过严谨的内存屏障和状态清理来保证在极端并发下的正确性。它是 HotSpot VM 并发控制技术的一个缩影。
更多推荐
所有评论(0)