LockStack在虚拟线程Mount和Unmount拷贝过程剖析

04-LockStack在虚拟线程Mount和Unmount拷贝过程剖析

前言

本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。

LockStack在虚拟线程Mount和Unmount拷贝过程

在 JDK 21+ 引入的轻量级锁(Fast Locking / JEP 428)架构中,HotSpot 废弃了传统依赖 Native 栈帧上 BasicObjectLock / Lock Record 链表的实现,改用直接嵌入 JavaThread 的 LockStack 数据结构。

当虚拟线程(VirtualThread)在 Carrier 线程(JavaThread)上执行 Mount (Thaw) 与 Unmount (Freeze) 时,LockStack 内部管理的对象指针(oop)必须在 Carrier 线程本地空间 与 **堆区 stackChunkOop** 之间进行剥离与还原。


一、 LockStack 核心 C++ 数据结构定义

LockStack 物理上直接内嵌在 JavaThread 对象中,避免了动态内存分配。

1. LockStack 结构定义 (src/hotspot/share/runtime/lockStack.hpp)

cpp 复制代码
class LockStack {
public:
  static const int CAPACITY = 8; // 线程本地轻量级锁栈容量(默认为 8)

private:
  // 直接存储锁定对象的 oop(指向 Java 堆对象的指针)
  oop _base[CAPACITY];
  // 栈顶指针,指示下一个空闲槽位的 offset (0 ~ CAPACITY)
  uint8_t _top;

public:
  LockStack() : _top(0) {
#ifdef ASSERT
    for (int i = 0; i < CAPACITY; i++) {
      _base[i] = nullptr;
    }
#endif
  }

  // 基础压栈与出栈
  void push(oop obj) {
    assert(_top < CAPACITY, "LockStack overflow");
    _base[_top++] = obj;
  }

  oop pop() {
    assert(_top > 0, "LockStack underflow");
    oop obj = _base[--_top];
    _base[_top] = nullptr;
    return obj;
  }

  // 从栈中特定位置移除锁对象(支持非严格 LIFO 的解锁顺序)
  void remove(oop obj);
  
  // 判断对象是否在当前线程的 LockStack 中
  bool contains(oop obj) const;

  // 暴露给 Freeze / Thaw 的指针与 Offset 接口
  oop* base() { return _base; }
  uint8_t top() const { return _top; }
  void set_top(uint8_t top) { _top = top; }
};

2. 在 JavaThread 中的内存内联 (src/hotspot/share/runtime/javaThread.hpp)

cpp 复制代码
class JavaThread : public Thread {
  // ...
private:
  LockStack _lock_stack; // 嵌入到 JavaThread 实例中的物理对象

public:
  LockStack& lock_stack() { return _lock_stack; }
  static ByteSize lock_stack_offset() {
    return byte_offset_of(JavaThread, _lock_stack);
  }
};

二、 Freeze (Unmount) 阶段:LockStack 的剥离与迁移

在虚拟线程挂起(Freeze)时,系统必须将 Carrier 线程 JavaThread::_lock_stack 中属于当前 Continuation 栈帧范围内的 oop 提取并转移 到堆上的 stackChunkOop,同时清理 Carrier 线程的 LockStack。

1. 挂起检查与 Pinning 校验 (continuationFreeze.cpp)

在执行数据拷贝前,HotSpot 首先通过 Freeze::check_pinned() 确认锁状态:

cpp 复制代码
// src/hotspot/share/runtime/continuationFreeze.cpp
template <typename Config>
class Freeze : public StackObj {
  JavaThread* const _thread;      // 当前 Carrier 线程
  ContinuationWrapper& _cont;     // 关联的 Continuation
  
  bool check_pinned() {
    // 1. 若使用了膨胀锁(Heavyweight Monitor),直接锁定 Pinning
    if (_thread->has_pending_monitors()) {
      return true; // 无法 Unmount
    }

    // 2. 检索 LockStack 中是否有受包含锁
    LockStack& lock_stack = _thread->lock_stack();
    if (lock_stack.top() > 0) {
      // 遍历 LockStack,校验锁定对象对应的栈帧是否在 Freeze 作用域内
      for (int i = 0; i < lock_stack.top(); i++) {
        oop obj = lock_stack.base()[i];
        if (is_locked_in_chunk_scope(obj)) {
          // 在 JDK 21 中属于轻量级锁且符合条件的锁允许转移,无需 Pin
        }
      }
    }
    return false;
  }
};

2. LockStack 提取与数据写入 C++ 源码

在确认可冻结后,通过 Freeze::freeze_lock_stack 进行数据剥离:

cpp 复制代码
// src/hotspot/share/runtime/continuationFreeze.cpp (概念化精简实现)
template <typename Config>
void Freeze::freeze_lock_stack(stackChunkOop chunk) {
  LockStack& lock_stack = _thread->lock_stack();
  int top = lock_stack.top();
  
  if (top == 0) return;

  int chunk_lock_count = 0;
  
  // 遍历 Carrier 线程当前的 LockStack
  for (int i = top - 1; i >= 0; i--) {
    oop obj = lock_stack.base()[i];
    
    // 校验该锁关联的栈帧是否属于即将打入 stackChunkOop 的范围
    if (_cont.is_in_frame_range(obj)) {
      // 1. 将 oop 写入 stackChunkOop 的锁对象存储区 (Lock Payload Zone)
      chunk->write_lock_at(chunk_lock_count++, obj);

      // 2. 从 Carrier 线程的 LockStack 中弹出/移除该对象
      lock_stack.remove(obj);
    }
  }

  // 更新 stackChunkOop 头部元数据中的锁计数器
  chunk->set_lock_count(chunk_lock_count);
}
剥离关键要点:
  • 必须物理移除 :若不清空 Carrier 线程 JavaThread::_lock_stack 中的 oop,后续其他虚拟线程被 Mount 到该 Carrier 上执行 monitorenter 时,会误以为自己已经持有该锁,导致并发保护机制失效。
  • 数据流向 :JavaThread::_lock_stack →\to→ stackChunkOop::lock_data()(堆内存连续区域)。

三、 Thaw (Mount) 阶段:LockStack 的还原与重新填充

当虚拟线程被 ForkJoinPool 的某一个 Carrier 线程选中,准备恢复执行(Thaw)时,系统需要将暂存在 stackChunkOop 里的 oop 重新还原 到新 Carrier 线程的 LockStack 中。

1. LockStack 还原 C++ 源码 (continuationThaw.cpp)

cpp 复制代码
// src/hotspot/share/runtime/continuationThaw.cpp
template <typename Config>
void Thaw::thaw_lock_stack(stackChunkOop chunk) {
  JavaThread* thread = _thread; // 当前接管执行的 Carrier 线程
  LockStack& lock_stack = thread->lock_stack();

  int chunk_lock_count = chunk->lock_count();
  if (chunk_lock_count == 0) return;

  // 确认 Carrier 线程的 LockStack 是否有足够空间(CAPACITY = 8)
  assert(lock_stack.top() + chunk_lock_count <= LockStack::CAPACITY, 
         "Carrier LockStack overflow during thaw!");

  // 从 stackChunkOop 依次读取并重新压入 Carrier 线程的 LockStack
  for (int i = 0; i < chunk_lock_count; i++) {
    oop obj = chunk->read_lock_at(i);
    
    if (obj != nullptr) {
      // 压入当前 Carrier 线程本地的 LockStack
      lock_stack.push(obj);
    }
  }

  // 清空 stackChunkOop 中的暂存锁记录,防止 GC 重复扫描
  chunk->set_lock_count(0);
}
还原关键要点:
  • 跨 Carrier 迁移 :Mount 时的 Carrier 线程物理上可能与 Unmount 时的 Carrier 线程完全不同。通过 thaw_lock_stack,锁持有者上下文无缝切换到了新的 JavaThread。
  • 次序一致性:压栈顺序必须严格保持原有的嵌套锁定层级(如外层锁先入栈、内层锁后入栈)。

四、 完整 C++ 内存数据流动图解

复制代码
  [ Carrier Thread A (JavaThread A) ]           [ Java Heap (stackChunkOop) ]
+------------------------------------+        +-------------------------------+
| _lock_stack:                      |        | stackChunkOop                 |
|   _base[0] = objA (Outer Lock)     |        |   _lock_count = 0             |
|   _base[1] = objB (Inner Lock)     |        |   _lock_payload[] = { null }  |
|   _top = 2                         |        +-------------------------------+
+-----------------+------------------+                        ^
                  |                                           |
                  | ====== [ Freeze / Unmount 剥离 ] =========|
                  | 1. 将 objA, objB 拷贝至 stackChunkOop
                  | 2. 执行 lock_stack.remove() 清空 Carrier A
                  v
+------------------------------------+        +-------------------------------+
| Carrier Thread A (JavaThread A)    |        | stackChunkOop                 |
| _lock_stack:                       |        |   _lock_count = 2             |
|   _base[0] = null                  |        |   _lock_payload[0] = objA     |
|   _top = 0  (变为空闲状态)          |        |   _lock_payload[1] = objB     |
+------------------------------------+        +-------------------------------+
                                                              |
                                                              |
  [ Carrier Thread B (JavaThread B) ]                         |
+------------------------------------+                        |
| Carrier Thread B (准备接管)         |                        |
| _lock_stack:                       |                        |
|   _top = 0                         |                        |
+-----------------+------------------+                        |
                  |                                           |
                  | ====== [ Thaw / Mount 还原 ] =============|
                  | 1. 从 stackChunkOop 读取 objA, objB
                  | 2. 执行 lock_stack.push() 还原至 Carrier B
                  v
+------------------------------------+        +-------------------------------+
| Carrier Thread B (JavaThread B)    |        | stackChunkOop                 |
| _lock_stack:                       |        |   _lock_count = 0 (重置)      |
|   _base[0] = objA                  |        |   _lock_payload[] = { null }  |
|   _base[1] = objB                  |        +-------------------------------+
|   _top = 2                         |
+------------------------------------+

五、 GC 屏障(OopMap)与指针自愈配合

当虚拟线程处于 Unmount 挂起状态时,锁对象 objA 与 objB 仅被存储在堆内的 stackChunkOop 中:

  1. GC Root 注册 :stackChunkOop 作为标准堆对象,在其 OopMap 中注册了内部保存的 lock_payload 偏移量。
  2. 并发标记与对象移动(以 ZGC / G1 为例):
  • 若 GC 在虚拟线程挂起期间触发了垃圾回收或对象压缩,GC 遍历到 stackChunkOop 时,会自动更新 lock_payload 中存放的 oop 物理地址(转向移动后的新地址)。
  1. Thaw 时获取自愈指针 :当虚拟线程在 Thaw 阶段恢复时,chunk->read_lock_at(i) 读取到的已经是经过 GC 修正、指向有效新内存地址的指针,直接写入 JavaThread::_lock_stack 即可,无需额外的指针修正步骤。
相关推荐
Felven1 小时前
B. Reverse a Permutation
c语言
樱花落木兰1 小时前
分布式登录实战:Session 会话共享改造,Redis 存储用户登录状态
java·javascript·数据库·redis·分布式·缓存
深念Y1 小时前
rime-雾凇拼音-配置记录
linux·junit·软件·拼音·kde
HAHAXX81 小时前
电商RPA批量上架通用方案:一套流程如何同时跑通拼多多、抖店、淘宝和跨境平台
java·运维·rpa
\光辉岁月/1 小时前
3.java运算符
java·开发语言
Logic1011 小时前
C语言/数据结构排序题解:三个数的最大乘积——排序后比较两种候选情况
c语言·数据结构·数组·排序·时间复杂度·算法题·负数处理
知识分享小能手1 小时前
C++ 学习教程,从入门到精通,string类和标准模板库 — 完整知识点详解(16)
开发语言·c++·学习
无忧.芙桃1 小时前
位图妙用:512MB 实现 40 亿整数的存在性查询
c语言·c++·面试·哈希算法