轻量锁分析见:揭秘Java synchronize:轻量级锁升级与偏向锁

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 的主要工作流程围绕 enterexitwaitnotify 四个核心操作展开。

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 ...
  1. 尝试自旋 (spin_enter):首先,代码会尝试通过自旋快速获取锁。如果 _owner 是 NO_OWNER,就通过 CAS(Compare-And-Swap)原子操作将 _owner 设置为当前线程。如果成功,直接返回。如果 _owner 就是当前线程自己,就增加 _recursions 计数,实现锁重入。
  2. 进入慢速路径(Contended Enter):如果自旋失败,说明存在真正的竞争。此时进入 enter_with_contention_mark
  3. 处理虚拟线程(Project Loom)
    // ... 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 ...
    
    这是对虚拟线程的特殊处理。平台线程阻塞会占用一个宝贵的 OS 线程资源。而虚拟线程在阻塞时,会尝试 卸载(unmount) 自己,即将 Java 栈保存到堆上,然后让其载体平台线程(Carrier Thread)去执行其他任务。Continuation::try_preempt 就是这个卸载操作的开始。
  4. 入队与阻塞(平台线程或虚拟线程卸载失败)
    • 线程被封装成一个 ObjectWaiter 节点。
    • 通过 CAS 操作,将这个节点“压”入 _entry_list 的头部。这是一个无锁操作,可以支持高并发的入队请求。
    • 调用 park() 方法(底层是 PlatformEvent::park 或 Parker::park),将当前线程挂起,等待被唤醒。
B. exit (释放锁)

exit 的逻辑相对简单,但包含关键的唤醒策略。

  1. 处理重入:如果 _recursions > 0,则只递减 _recursions 计数,不释放锁。
  2. 释放锁:当 _recursions 为 0 时,将 _owner 设置为 NO_OWNER
  3. 唤醒继承者:这是 exit 的关键。
    • 它会检查 _entry_list 是否为空。
    • 如果不为空,它会从队列中挑选一个线程作为继承者(通常是队尾的线程,实现 FIFO)。
    • 这个挑选过程非常精妙,如文件顶部的注释所述,如果 _entry_list_tail 未知,它会遍历 _entry_list 这个单向链表,并在遍历过程中设置 prev 指针,将其临时变成一个双向链表,从而高效地找到队尾。
    • 找到继承者后,将其记录在 _succ 字段,并调用 unpark() 唤醒它。
C. wait & notify

这两个操作与 _wait_set 队列紧密相关。

  • wait()

    1. 将当前线程加入到 _wait_set 队列中。
    2. 完全释放锁(包括所有重入),将 _owner 设为 NO_OWNER
    3. 唤醒一个 _entry_list 中的线程,让它来竞争锁。
    4. 调用 park() 挂起当前线程,等待 notify
    5. 被唤醒后,线程会从 _wait_set 移到 _entry_list 中,重新开始竞争锁。
  • notify() / notifyAll()

    1. notify() 从 _wait_set 中取出一个线程(通常是等待最久的)。
    2. notifyAll() 取出 _wait_set 中所有的线程。
    3. 关键:这些被取出的线程并不会被立即唤醒,而是被移动到 _entry_list 队列的尾部。
    4. 为什么不直接唤醒?因为 notify 的调用者仍然持有锁。如果直接唤醒等待线程,它会立刻尝试获取锁,但必然失败并再次阻塞,这被称为“无用的唤醒”(futile wakeup)。将其移入 _entry_list,等 notify 调用者执行完 exit 释放锁后,再由 exit 的逻辑来唤醒,这样就保证了公平性和效率。

总结:ObjectMonitor 的设计哲学

ObjectMonitor 是一个精巧而复杂的 C++ 实现,体现了以下设计哲学:

  1. 混合使用无锁与有锁:在线程争抢进入 _entry_list 时,使用无锁的 CAS 操作来提高并发性。而在队列内部操作(如出队)时,则依赖于持有 ObjectMonitor 锁的线程来保证安全,避免了更复杂的无锁算法。
  2. 公平性策略:通过 _entry_list 和 _wait_set 两个队列,并采用 FIFO 策略,尽可能保证线程饥饿问题不会发生。
  3. 性能优化:通过自旋、将 notify 的线程移入 entry_list 而非直接唤醒等策略,减少不必要的线程上下文切换和系统调用。
  4. 面向未来的设计:对虚拟线程的集成支持,表明了其设计在不断演进,以适应 JVM 的最新发展(如 Project Loom)。
  5. 健壮性优先:相比 Parker 的简洁,ObjectMonitor 及其关联的 ParkEvent 使用了更复杂的机制(如三态状态机、不朽对象池),根本目的是为了在 synchronized 这种基础、复杂的场景下保证绝对的线程安全和状态一致性。

ObjectMonitor队列设计的必要性分析

ObjectMonitor 维护队列(如 EntryList、WaitSet)而非直接使用 mutex 的 lock/wait/signal,主要原因如下:

  1. ​支持 Java 层语义(wait/notify)​

    • Java 的 Object.wait()/notify()/notifyAll()要求线程有序进入等待和唤醒状态。

    • wait()需将线程放入等待队列(WaitSet),等待被 notify()notifyAll()唤醒。

    • notify()仅唤醒一个等待线程,notifyAll()唤醒所有等待线程。

    • 仅用 mutex 的 lock/wait/signal无法区分:

      • 正在竞争锁的线程

      • 调用 wait()进入等待的线程

    • 无法实现 Java 的精确唤醒语义。

  2. ​支持公平性与高效性​

    • 队列可保证线程唤醒的顺序性(如 FIFO)和公平性。

    • 直接使用 mutex 时:

      • 线程唤醒顺序不可控

      • 易引发“惊群效应”(thundering herd),降低性能。

  3. ​支持多状态管理​

    • ObjectMonitor 需区分以下线程状态:

      • ​EntryList​​:正在竞争锁的线程

      • ​WaitSet​​:已调用 wait()的线程

      • ​Owner​​(支持可重入):当前持有锁的线程

    • 队列是实现这些状态管理的必要机制,以正确支持 synchronizedwait/notify的复杂语义。

  4. ​互斥锁的 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的队列设计是为了:

  1. 弥合Java同步语义与操作系统原语之间的语义鸿沟
  2. 提供高性能的用户态同步优化
  3. 支持现代JVM特性(如虚拟线程)
  4. 实现精确的线程状态管理和公平性控制

简单的mutex lock/wait/signal无法满足Java Monitor的复杂语义要求,队列机制是实现这些高级特性的必要基础设施。

ObjectMonitor 与 CLH、AQS

ObjectMonitor 的实现与 CLH(Craig, Landin, and Hagersten 队列锁)和 AQS(AbstractQueuedSynchronizer,Java并发包的核心同步器)有相似之处,但也有明显不同。下面简要分析:

相似点

  1. ​队列管理等待线程​

    • ObjectMonitor 通过 _entry_list(进入队列)和 _wait_set(等待队列)管理等待锁和等待条件的线程。
    • CLH/AQS 都是通过链表(队列)管理等待线程,AQS 也是通过一个双向链表(CLH 是隐式队列,AQS 是显式队列)来管理。
  2. ​公平性和唤醒策略​

    • ObjectMonitor 选择队列尾部的线程作为"继任者"唤醒,保证一定的公平性。
    • AQS 也是通过队列唤醒下一个节点,CLH 也是类似的"排队"思想。
  3. ​自旋与阻塞结合​

    • ObjectMonitor 先尝试自旋获取锁,失败后才进入阻塞队列。
    • AQS/CLH 也有类似的自旋-阻塞混合策略(AQS 支持自旋,CLH 本身就是自旋锁)。
  4. ​CAS 操作​

    • 都大量使用 CAS(Compare-And-Swap)来保证队列和锁状态的原子性。

不同点

  1. ​队列结构​

    • ObjectMonitor 的 _entry_list 是单链表,必要时转为双链表,且队列头尾指针管理较为复杂。
    • AQS 是显式的双向链表,节点结构更清晰,且有头尾指针。
    • CLH 是隐式队列,每个线程持有自己的节点和前驱节点的引用。
  2. ​锁的传递方式​

    • ObjectMonitor 并不直接"传递"锁,而是采用竞争式唤醒(竞争式 handoff),即唤醒后线程还需重新竞争锁。
    • AQS和Object Monitor类似。AQS 唤醒线程后,线程还需要重新竞争锁(不是直接获得锁)。AQS 通过条件队列(Condition Queue)机制,实现了类似 ObjectMonitor 的 wait/notify/notifyAll 功能。
    • CLH 是严格的 FIFO,自旋在前驱节点上,前驱释放后自己获得锁。
  3. ​虚拟线程支持​

    • ObjectMonitor 针对虚拟线程(vthread)做了大量特殊处理(如挂起、解挂、特殊的唤醒机制),AQS/CLH 没有这部分内容。
  4. ​wait/notify 支持​

    • ObjectMonitor 原生支持 Java 的 wait/notify/notifyAll 语义,AQS 需要配合 ConditionObject 实现,CLH 本身不直接支持条件变量。
  5. ​实现复杂度​

    • 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 代码块,并且该对象的锁已经膨胀为重量级锁时,就会调用这个函数。

它的设计遵循一个分层优化的策略

  1. 快速路径 (Fast Path):尝试通过自旋 (Spinning) 快速获取锁,避免线程阻塞带来的巨大开销。
  2. 慢速路径 (Slow Path):如果自旋失败,说明存在真正的锁竞争。此时,线程将进入一个复杂的流程,最终可能会被挂起(park),加入等待队列。
  3. 健壮性与协同:整个过程必须处理好与 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 内部主要做几件事:

  1. 检查重入 (try_enter):首先判断当前线程是否已经是锁的持有者 (has_owner(current))。如果是,就简单地将 _recursions 字段加一,然后直接返回 true。这是 synchronized 可重入特性的实现。
  2. 尝试获取锁 (try_lock):如果锁没有持有者 (_owner == NO_OWNER),它会通过 CAS (Compare-And-Swap) 原子操作,尝试将 _owner 设置为当前线程。如果成功,获取锁,返回 true
  3. 检查锁降级冲突:检查 ObjectMonitor 是否正在被异步降级 (enter_is_async_deflating)。如果是,就放弃本次操作,返回 false,让上层重试。
  4. 自旋 (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;
}
  1. ObjectMonitorContentionMark contention_mark(this);

    • 这是一个非常重要的 RAII (Resource Acquisition Is Initialization) 对象。
    • 在它的构造函数中,会原子地将 _contentions 字段加一。
    • 这个字段的作用是向 VM 的其他部分(特别是锁降级线程)声明:“这个 Monitor 正有线程在激烈竞争,请不要对它进行降级操作!”
    • 当 contention_mark 对象离开作用域时,它的析构函数会自动将 _contentions 减一。
  2. 再次检查锁降级 (enter_is_async_deflating)

    • 在标记了 _contentions 之后,再次进行检查。这次检查更加可靠,因为它与降级线程之间有了一个明确的“协议”。如果此时发现仍在降级,就返回 false,让上层代码(在 ObjectSynchronizer::slow_enter 中)重试整个 enter 过程。
  3. 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 而新增的复杂逻辑。

  1. if (is_virtual):检查当前线程是否为虚拟线程。
  2. Continuation::try_preempt(...)这是关键。它尝试抢占 (preempt) 并卸载 (unmount) 当前的虚拟线程。
    • 卸载:将虚拟线程的 Java 调用栈数据从其载体平台线程(Carrier Thread)的栈上复制到 Java 堆中。
    • 释放:载体平台线程被释放,可以去执行其他的任务。
    • 这样,虚拟线程的“阻塞”就不会真正占用一个宝贵的操作系统线程,极大地提高了系统的吞吐量。
  3. vthread_monitor_enter(current):虚拟线程被成功卸载后,这个函数负责将其加入到 _entry_list 等待队列中。
  4. 竞争与取消:如果在尝试卸载的过程中,锁恰好被释放了,vthread_monitor_enter 可能会直接获取到锁。此时,它会设置一个“取消抢占”的标志,阻止卸载的发生,让虚拟线程继续在原平台线程上执行。

B. 平台线程 (Platform Thread) 分支

这是传统的线程阻塞路径。

  1. JavaThreadBlockedOnMonitorEnterState jtbmes(...):又一个 RAII 对象,它负责将线程的状态设置为 _thread_blocked_on_monitor_enter。这样,当使用 jstack 等工具查看线程堆栈时,就能明确看到该线程正在等待进入一个监视器。
  2. 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(;;) 循环中出来后,它已经成功持有了锁。

  1. 状态断言:一系列 assert 语句确保 _recursions 为 0,_owner 是当前线程等状态的正确性。
  2. 事件通知:调用 DTRACE_MONITOR_PROBE 和 JvmtiExport::post_monitor_contended_entered 等函数,发布 JFR、DTrace、JVMTI 事件。这使得外部工具可以观测到锁竞争和获取的详细情况。
  3. 虚拟线程固定事件 (Pinned Event):如果一个虚拟线程因为某些原因未能成功卸载,而是像平台线程一样阻塞了其载体线程,这里会发布一个“线程被固定 (pinned)”的事件。这对诊断虚拟线程的性能问题至关重要。

总结

ObjectMonitor::enter 是一个集性能优化、多场景健壮性处理和未来技术(虚拟线程)支持于一体的典范。其逻辑可以概括为:

  • 乐观尝试:通过自旋,用最小的代价获取锁。
  • 悲观准备:进入慢速路径,通过 _contentions 标记来防止降级冲突。
  • 分类处理:为虚拟线程提供高效的“卸载”机制,为平台线程提供安全的“挂起”机制。
  • 安全第一:通过 RAII 和严谨的状态检查,确保在线程挂起、锁降级等复杂并发场景下的数据一致性和无死锁。
  • 高度可观测:深度集成 JFR、DTrace、JVMTI 等监控工具。

ObjectMonitor::enter_internal 

当一个线程经过了快速路径(spin_enter)的多次尝试仍然无法获取锁之后,就会进入这个函数,准备进行排队和阻塞。它被 enter_with_contention_mark 调用,此时线程状态已经被标记为 _thread_blocked

enter_internal 的策略可以概括为“最后的挣扎,然后体面地排队睡觉”。

  1. 最后的挣扎:在将线程加入等待队列(_entry_list)之前,再进行最后几轮的 try_lock 和 try_spin。因为入队和出队是有开销的,能避免则尽量避免。
  2. 体面地排队:如果实在获取不到锁,就将自己封装成一个 ObjectWaiter 节点,通过一个高度优化的 try_lock_or_add_to_entry_list 操作加入队列。
  3. 安全地睡觉:进入一个循环,调用 park() 方法挂起线程。这个循环必须处理好各种唤醒情况(正常唤醒、伪唤醒、虚拟线程超时唤醒)并保证最终能安全地获取锁。
  4. 干净地离场:成功获取锁后,将自己的节点从队列中移除,并清理状态。

详细代码流程分析

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 ...
第一阶段:入队前的最后尝试

这是进入阻塞流程前的最后机会。

  1. if (try_lock(current) == TryLockResult::Success)

    • 这是经典的“Test-And-Test-And-Set” (TATAS) 优化中的一环。在进入更昂贵的自旋之前,先简单地测试一下锁是否可用。如果恰好锁被释放了,就能立即获取并返回,避免了后续所有开销。
  2. 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 ...
  1. ObjectWaiter node(current);

    • 在当前线程的栈上创建一个 ObjectWaiter 对象。这个对象是线程在 _entry_list 中的代理。栈上分配避免了堆分配的开销和管理复杂性。
  2. current->_ParkEvent->reset();

    • 重置与当前线程关联的 ParkEventParkEvent 是实现线程挂起/唤醒的底层机制。reset() 将其内部状态清理干净,确保 park() 行为符合预期。
  3. 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 ...
  1. for (;;) 循环:线程将在此循环,直到成功获取锁。

  2. 入循环后立即 try_lock:这是为了解决一个经典的**“信号丢失”**竞争问题。设想一下:线程 A 刚刚把自己加入队列,但还没来得及 park。此时,持有锁的线程 B exit,检查 _entry_list,发现了 A,于是 unpark(A),然后走掉。如果 A 此后直接 park,它将永远等待,因为唤醒信号已经错过了。所以在 park 之前必须再检查一次锁。

  3. 虚拟线程的特殊处理 (do_timed_parked)

    • 如果当前是被固定 (pinned) 的虚拟线程,它不能无限期地 park,因为这会永久占用一个宝贵的平台载体线程。虚拟线程的「固定(Pinned)」场景​​,虚拟线程​​通常由JVM调度到​​平台线程(Carrier Thread)​​上执行,但某些操作(如执行native代码或持有某些锁时)会导致虚拟线程被​​固定(Pinned)​​到当前平台线程,无法被卸载。风险​​:如果大量被固定的虚拟线程无限期地park()(阻塞),它们会永久占用平台线程(操作系统线程),而平台线程的数量是有限的(通常等于CPU核心数或稍多)。
    • 它会使用带超时的 park()。第一次超时很短(1ms),然后指数级增加,直到一个上限(1000ms)。
    • 目的:这是一种死锁规避机制。想象一个场景:所有平台线程都被固定住了,都在等待一个由尚未运行的虚拟线程持有的资源(例如类加载锁)。如果所有线程都无限期 park,系统就死锁了。定期的超时唤醒给了系统一个“喘息”的机会,让调度器有机会去运行那个关键的虚拟线程。
  4. 平台线程的 park():对于普通平台线程,直接调用 current->_ParkEvent->park(),无限期等待,直到被 unpark

  5. 唤醒后的逻辑:当 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;
}
  1. unlink_after_acquire(current, &node):将当前线程的 ObjectWaiter 节点从 _entry_list 中安全地移除。这是一个需要小心处理链表指针的操作。
  2. if (has_successor(current)) clear_successor():再次检查并清理 _succ 状态。因为持有锁,所以这里的操作是线程安全的,不需要内存屏障。
  3. JMM 保证:函数末尾的大段注释解释了Java内存模型(JMM)的保证。简单来说,获取锁的操作(CAS)带有“acquire”语义,而后续释放锁的操作(exit)带有“release”语义。这确保了线程在持有锁期间对内存的所有修改,对于下一个获取到该锁的线程都是可见的,从而保证了 synchronized 的内存可见性。

总结

ObjectMonitor::enter_internal 是一个集多种优化与健壮性设计于一体的函数。它通过多轮次的自旋和尝试来避免阻塞,通过原子操作和精巧的算法来安全地管理等待队列,通过对虚拟线程的特殊处理来适应未来的并发模型,并通过严谨的内存屏障和状态清理来保证在极端并发下的正确性。它是 HotSpot VM 并发控制技术的一个缩影。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐