Virtual Thread重投递至ForkJoinPool任务队列过程解析
- 前言
- [Virtual Thread重投递至ForkJoinPool任务队列过程](#Virtual Thread重投递至ForkJoinPool任务队列过程)
-
- [Virtual Thread Wake-Up Architecture](#Virtual Thread Wake-Up Architecture)
- [一、 Linux 内核态:从网卡中断到 `epoll_wait` 唤醒](#一、 Linux 内核态:从网卡中断到
epoll_wait唤醒) -
- [1. 套接字数据就绪与回调触发 (`net/core/sock.c`)](#1. 套接字数据就绪与回调触发 (
net/core/sock.c)) - [2. `epoll` 唤醒回调逻辑 (`fs/eventpoll.c`)](#2.
epoll唤醒回调逻辑 (fs/eventpoll.c))
- [1. 套接字数据就绪与回调触发 (`net/core/sock.c`)](#1. 套接字数据就绪与回调触发 (
- [二、 JVM 用户态 Poller 线程事件接收 (C++ / JNI / Java)](#二、 JVM 用户态 Poller 线程事件接收 (C++ / JNI / Java))
-
- [1. Native 层 `epoll_wait` 绑定 (`EPoll.c`)](#1. Native 层
epoll_wait绑定 (EPoll.c)) - [2. Java Poller 线程循环与 Request 映射 (`Poller.java`)](#2. Java Poller 线程循环与 Request 映射 (
Poller.java))
- [1. Native 层 `epoll_wait` 绑定 (`EPoll.c`)](#1. Native 层
- [三、 `VirtualThread` 状态机变更与 Continuation 恢复提交](#三、
VirtualThread状态机变更与 Continuation 恢复提交) -
- [1. 虚拟线程原子状态机 (`VirtualThread.java`)](#1. 虚拟线程原子状态机 (
VirtualThread.java))
- [1. 虚拟线程原子状态机 (`VirtualThread.java`)](#1. 虚拟线程原子状态机 (
- [四、 `ForkJoinPool` 外部任务入队与 Carrier 线程唤醒](#四、
ForkJoinPool外部任务入队与 Carrier 线程唤醒) -
- [1. 外部入队与环形缓冲区操作 (`ForkJoinPool.java`)](#1. 外部入队与环形缓冲区操作 (
ForkJoinPool.java)) - [2. 唤醒空闲 Carrier 线程 (`signalWork`)](#2. 唤醒空闲 Carrier 线程 (
signalWork))
- [1. 外部入队与环形缓冲区操作 (`ForkJoinPool.java`)](#1. 外部入队与环形缓冲区操作 (
- [五、 Carrier 线程挂载 (Mount) 与 Native 栈帧解冻 (Thaw)](#五、 Carrier 线程挂载 (Mount) 与 Native 栈帧解冻 (Thaw))
-
- [1. 唤醒后重新执行非阻塞 Syscall (`SocketChannelImpl.java`)](#1. 唤醒后重新执行非阻塞 Syscall (
SocketChannelImpl.java))
- [1. 唤醒后重新执行非阻塞 Syscall (`SocketChannelImpl.java`)](#1. 唤醒后重新执行非阻塞 Syscall (
- [六、 完整唤醒链路时序图汇总](#六、 完整唤醒链路时序图汇总)
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
Virtual Thread重投递至ForkJoinPool任务队列过程
Project Loom 中,虚拟线程(Virtual Thread)将"阻塞式编程"转换为底层"异步非阻塞 I/O"。当虚拟线程因等待 Socket 读写而挂起时,底层 Carrier 线程(JavaThread)会被释放。当硬件网卡接收到数据后,系统将沿着 Linux Kernel 内核中断 → \to → JVM Native Poller 线程 → \to → Java 态 VirtualThread.unpark() → \to → ForkJoinPool 任务队列 的全链路唤醒虚拟线程。
Virtual Thread Wake-Up Architecture
========================================================================================================================
Loom Virtual Thread Wake-Up Architecture
========================================================================================================================
[ Linux Kernel Space ]
Hardware IRQ -> softirq (NET_RX_SOFTIRQ) -> TCP/IP Stack -> sk_receive_queue
-> sock_def_readable() -> ep_poll_callback() -> Wakeup Poller Thread in ep_poll()
│
▼ (Syscall return)
[ OpenJDK JVM Poller (C++ / JNI) ]
Java_sun_nio_ch_EPoll_wait() -> epoll_wait() unblocks
│
▼
[ OpenJDK Poller (Java Space) ]
Poller$PollerThread.run() -> Poller.polled() -> Lookup Poller.Request
-> LockSupport.unpark(vthread)
│
▼
[ Virtual Thread State Machine ]
VirtualThread.unpark() -> CAS State: PARKED -> RUNNABLE
-> VirtualThread.submitRunContinuation()
│
▼
[ ForkJoinPool Scheduler ]
ForkJoinPool.externalPush(runContinuation) -> Submission WorkQueue [ring buffer]
-> signalWork() -> futex_wake() / Unsafe.unpark(carrierThread)
│
▼ (Carrier Thread Woken Up)
[ Carrier Thread & Continuation Thaw ]
ForkJoinWorkerThread.run() -> Pop runContinuation -> Continuation.run()
-> Continuation::thaw() [C++ Assembly Stub] -> Restore stack frames from stackChunkOop
-> Return to Java SocketChannelImpl.read() -> Re-issue sys_read() [SUCCESS]
========================================================================================================================
一、 Linux 内核态:从网卡中断到 epoll_wait 唤醒
数据包到达网卡后,Linux 内核通过中断与 socket 唤醒链表解锁阻塞在 epoll_wait 的 JVM Poller 线程。
+-------------------+ +----------------------+ +-----------------------+
| NIC HW Interrupt | ---> | NET_RX_SOFTIRQ | ---> | tcp_v4_rcv() |
| (Data Packet) | | (NAPI Poll Processing| | (TCP State Machine) |
+-------------------+ +----------------------+ +-----------------------+
|
v
+-------------------+ +----------------------+ +-----------------------+
| ep_poll_callback | <--- | wake_up_interruptible| <--- | sock_def_readable() |
| (Add to rdllist) | | _sync_poll() | | (sk_data_ready) |
+-------------------+ +----------------------+ +-----------------------+
1. 套接字数据就绪与回调触发 (net/core/sock.c)
当 TCP 协议栈完成数据包重组并将 sk_buff 放入 socket 接收队列(sk_receive_queue)时,内核调用 socket 的数据就绪回调函数 sk_data_ready(默认指向 sock_def_readable):
c
// Linux Kernel: net/core/sock.c
void sock_def_readable(struct sock *sk)
{
struct socket_alloc *si;
rcu_read_lock();
// 获取挂载在 socket 上的等待队列头 (wait_queue_head_t)
struct socket_wq *wq = rcu_dereference(sk->sk_wq);
if (skwq_has_sleeper(wq)) {
// 触发等待队列上的回调函数,唤醒关联的 epoll 项
wake_up_interruptible_sync_poll(&wq->wait, EPOLLIN | EPOLLPRI | EPOLLRDNORM | EPOLLRDBAND);
}
sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);
rcu_read_unlock();
}
2. epoll 唤醒回调逻辑 (fs/eventpoll.c)
当 epoll_ctl(EPOLL_CTL_ADD) 将 socket 注册到 epoll 句柄时,内核为该 socket 挂载了一个 epitem,并将 ep_poll_callback 注册为唤醒回调。
c
// Linux Kernel: fs/eventpoll.c
static int ep_poll_callback(wait_queue_entry_t *wait, unsigned mode, int sync, void *key)
{
int pwake = 0;
struct epitem *epi = ep_item_from_wait(wait); // 获取包含此 socket 的 epitem
struct eventpoll *ep = epi->ep;
uintptr_t pollflags = (uintptr_t)key;
spin_lock_irqsave(&ep->lock, flags);
// 1. 检查事件掩码是否匹配 (例如是否触发了 EPOLLIN)
if (pollflags && !(pollflags & epi->event.events))
goto out_unlock;
// 2. 将就绪的 epitem 追加到 epoll 的就绪链表 rdllist 中
if (!ep_is_linked(epi)) {
list_add_tail(&epi->rdllnk, &ep->rdllist);
}
// 3. 如果 epoll_wait 线程正处于 SLEEPING 状态 (等待在 ep->wq 队列上)
if (waitqueue_active(&ep->wq)) {
// 唤醒阻塞在 sys_epoll_wait 的 JVM Poller 线程
wake_up_locked(&ep->wq);
}
out_unlock:
spin_unlock_irqrestore(&ep->lock, flags);
return 1;
}
内核中的 wake_up_locked(&ep->wq) 将处于 TASK_INTERRUPTIBLE 状态的 JVM Poller 线程的 task_struct 重新变更为 TASK_RUNNING,并将其放入 CFS / EEVDF 运行队列。调度器选中该线程后,系统调用 sys_epoll_wait 返回就绪的事件数量。
二、 JVM 用户态 Poller 线程事件接收 (C++ / JNI / Java)
JVM Poller 是一个独立的 C++ / Java 平台线程(非 Virtual Thread),专门负责轮询 epoll_fd。
[ Linux Kernel ] ---> epoll_wait() 返回
|
v
[ EPoll.c (JNI) ] ---> Java_sun_nio_ch_EPoll_wait() 填入 address 内存数组
|
v
[ Poller.java ] ---> Poller$PollerThread.run()
|
v
Poller.polled(fd, events)
|
v
LockSupport.unpark(vthread)
1. Native 层 epoll_wait 绑定 (EPoll.c)
Poller 线程通过 JNI 循环调用本地 epoll_wait 系统调用:
c
// OpenJDK: src/java.base/unix/native/libnio/ch/EPoll.c
JNIEXPORT jint JNICALL
Java_sun_nio_ch_EPoll_wait(JNIEnv *env, jclass clazz, jint epfd,
jlong address, jint numfds, jint timeout)
{
// 将传入的 Java DirectBuffer 地址强转为 struct epoll_event 数组
struct epoll_event *events = (struct epoll_event *)jlong_to_ptr(address);
int res;
RESTARTABLE(epoll_wait(epfd, events, numfds, timeout), res);
if (res < 0) {
JNU_ThrowIOExceptionWithLastError(env, "epoll_wait failed");
return -1;
}
return res; // 返回捕获的就绪文件描述符数量
}
2. Java Poller 线程循环与 Request 映射 (Poller.java)
sun.nio.ch.Poller(位于 java.base/sun/nio/ch/Poller.java)提取底层返回的就绪事件数组,通过 fd 索引找出对应的等待请求 Poller.Request:
java
// OpenJDK: src/java.base/share/classes/sun/nio/ch/Poller.java
package sun.nio.ch;
public abstract class Poller {
// Poller 专属后台线程
private class PollerThread extends Thread {
public void run() {
for (;;) {
// 1. 调用 JNI 的 epoll_wait() 阻塞等待事件,结果存入 address 缓冲区
int numevents = EPoll.wait(epfd, address, MAX_EVENTS, timeout);
for (int i = 0; i < numevents; i++) {
long eventAddr = EPoll.getEvent(address, i);
int fd = EPoll.getDescriptor(eventAddr);
int ev = EPoll.getEvents(eventAddr);
// 2. 根据 fd 响应就绪事件,解绑事件并处理
polled(fd, ev);
}
}
}
}
// 响应事件的核心处理逻辑
protected final void polled(int fd, int ev) {
// 根据 fd 从哈希表/数组中找出之前 parked 的 Poller.Request
Request request = map.remove(fd);
if (request != null) {
// 获取绑定在该请求上的 VirtualThread 引用
Thread thread = request.thread();
// 3. 唤醒被挂起的 VirtualThread
LockSupport.unpark(thread);
}
}
}
三、 VirtualThread 状态机变更与 Continuation 恢复提交
LockSupport.unpark(thread) 将引发 VirtualThread 内部原子状态机的转换,并将该虚拟线程的 runContinuation 提交至 ForkJoinPool。
[ LockSupport.unpark() ]
|
v
[ VirtualThread.unpark() ]
|
+---> CAS(State): PARKED -> RUNNABLE
|
v
[ submitRunContinuation() ]
|
v
[ scheduler.execute(runContinuation) ] (提交至 ForkJoinPool)
1. 虚拟线程原子状态机 (VirtualThread.java)
虚拟线程在挂起与唤醒过程中使用 volatile int state 结合 Unsafe CAS 保证线程安全:
java
// OpenJDK: src/java.base/share/classes/java/lang/VirtualThread.java
package java.lang;
public class VirtualThread extends Thread {
// 虚拟线程状态常量
private static final int NEW = 0;
private static final int STARTED = 1;
private static final int RUNNING = 2; // 正在 Carrier 线程上运行
private static final int PARKING = 3; // 正在 Unmount/Freeze 过程中
private static final int PARKED = 4; // 已挂起,在堆中休眠
private static final int PINNED = 5; // 钉住状态
private static final int TIMED_PARK = 6;
private volatile int state;
private final Runnable runContinuation; // 用于包装 Continuation.run() 的任务
private final Executor scheduler; // 通常为默认的 ForkJoinPool
@Override
void unpark() {
if (!Thread.currentThread().equals(this)) {
// 外部线程(如 Poller 线程)调用唤醒
int s = state;
// 1. 尝试通过 CAS 将状态从 PARKED 变更为 RUNNABLE
while (s == PARKED || s == TIMED_PARK) {
if (STATE.compareAndSet(this, s, RUNNABLE)) {
// 2. CAS 变更成功,将任务提交至调度器
submitRunContinuation();
break;
}
s = state; // 重新读取最新状态 retry
}
}
}
private void submitRunContinuation() {
try {
// 3. 将 runContinuation 重新提交给 ForkJoinPool
scheduler.execute(runContinuation);
} catch (RejectedExecutionException ree) {
// 异常兜底逻辑
}
}
}
四、 ForkJoinPool 外部任务入队与 Carrier 线程唤醒
由于 PollerThread 属于非 ForkJoinWorker 的外部平台线程(External Thread),它必须通过 ForkJoinPool.externalPush 将 runContinuation 压入无锁的 Submission Queue(提交队列)。
PollerThread (External non-FJP thread)
|
v
[ ForkJoinPool.externalPush(runContinuation) ]
|
v
[ Find/Create WorkQueue (Submission Queue) ]
|
v
[ Push into WorkQueue.array (Ring Buffer via CAS) ]
|
v
[ ForkJoinPool.signalWork() ]
|
v
[ Unsafe.unpark(Carrier Thread) / futex_wake() ]
1. 外部入队与环形缓冲区操作 (ForkJoinPool.java)
java
// OpenJDK: src/java.base/share/classes/java/util/concurrent/ForkJoinPool.java
package java.util.concurrent;
public class ForkJoinPool extends AbstractExecutorService {
// 外部线程提交任务入口
final void externalPush(ForkJoinTask<?> task) {
WorkQueue q;
// 获取外部提交线程对应的随机 WorkQueue 槽位
int probe = ThreadLocalRandom.getProbe();
if (probe == 0) {
ThreadLocalRandom.localInit();
probe = ThreadLocalRandom.getProbe();
}
// 1. 找到对应的 submission WorkQueue (偶数索引槽位)
if ((q = findSubmissionQueue(probe)) != null) {
// 2. 将 task 压入该队列的 array 数组中
q.push(task, this);
return;
}
// 慢速初始化队列并入队
externalSubmit(task);
}
// WorkQueue 内部的压栈逻辑 (非锁 Ring Buffer)
static final class WorkQueue {
volatile int top; // 栈顶指针 (仅 Push 线程可更新)
int base; // 栈底指针
ForkJoinTask<?>[] array; // 任务环形数组
final void push(ForkJoinTask<?> task, ForkJoinPool pool) {
int s = top;
ForkJoinTask<?>[] a = array;
if (a != null) {
int size = a.length;
int cap = size - 1;
// 1. 计算元素在数组中的内存偏移量 Offset
int index = s & cap;
// 2. 将 runContinuation 写入 Ring Buffer 对应槽位
QA.setRelease(a, index, task);
// 3. 递增 top 指针 (Release 语义保障可见性)
TOP.setRelease(this, s + 1);
// 4. 唤醒或创建空闲的 Carrier Worker 线程去 Steal 任务
pool.signalWork();
}
}
}
}
2. 唤醒空闲 Carrier 线程 (signalWork)
signalWork() 通过解析 ForkJoinPool 内部的 64 位 atomic ctl 状态字段,寻找处于休眠状态(AC_MASK / TC_MASK 控制)的 Worker 线程,并对其执行 LockSupport.unpark()(底层在 Linux 上对应 sys_futex(FUTEX_WAKE)):
java
// OpenJDK: src/java.base/share/classes/java/util/concurrent/ForkJoinPool.java
final void signalWork() {
long c;
// 检查是否有空闲线程 (ctl 包含平摊活跃线程数与空闲链表 Head 指针)
while ((c = ctl) < 0L) {
int sp = (int)c; // 获取休眠栈顶 Worker 的 ID
if (sp == 0) {
// 没有处于休眠状态的 Worker,检查是否需要创建新的 Carrier 线程
if ((c & ADD_WORKER) != 0L)
tryAddWorker(c);
break;
}
// 尝试 CAS 弹出现有的休眠 Worker
WorkQueue[] qes = queues;
int i = sp & SMASK;
if (qes != null && i < qes.length && qes[i] != null) {
WorkQueue v = qes[i];
long nc = (v.stackPred & SP_MASK) | ((c + AC_UNIT) & ~SP_MASK);
if (CTL.compareAndSet(this, c, nc)) {
v.phase = sp;
// 获取绑定的 Carrier 平台线程对象 (ForkJoinWorkerThread)
Thread p = v.owner;
if (p != null) {
// 唤醒该 Carrier 线程
LockSupport.unpark(p);
break;
}
}
}
}
}
五、 Carrier 线程挂载 (Mount) 与 Native 栈帧解冻 (Thaw)
Carrier 线程被唤醒后,从 WorkQueue 弹出 runContinuation 并运行,最终进入 C++ 层的 Continuation::thaw,将 stackChunkOop 内的 Java 栈帧重新写回当前 Carrier 线程的物理 Native Stack。
[ Carrier Thread (ForkJoinWorkerThread) ]
|
v
Executes runContinuation.run()
|
v
jdk.internal.vm.Continuation.run()
|
v
Continuation::thaw() [C++ Native Code / Assembly Stub]
|
+---> 1. 从 stackChunkOop 反序列化栈帧
+---> 2. 将 JIT / Interpreter 栈帧写回 Carrier Native Stack
+---> 3. 调整硬件 RSP / RBP 寄存器指针
+---> 4. 设置 Thread.currentThread() = VirtualThread
+---> 5. JMP 至挂起时的 _pc (RIP) 地址
|
v
[ Java Land: SocketChannelImpl.read() ]
|
v
Re-issue sys_read(fd, buf, len) --->【返回读到的真正数据 SUCCESS】
1. 唤醒后重新执行非阻塞 Syscall (SocketChannelImpl.java)
以网络读操作为例,当 Virtual Thread 恢复执行后,它会从上次 Unmount 离开的指令地址(_pc)继续向下运行,即跳出 park() 循环并再次调用 read():
java
// OpenJDK: src/java.base/share/classes/sun/nio/ch/SocketChannelImpl.java
public int read(ByteBuffer buf) throws IOException {
for (;;) {
// 1. 尝试发起 sys_read 系统调用
int n = Tokens.read(fd, buf);
if (n == IOStatus.UNAVAILABLE) {
// 如果是在 Mount 前,会在这里将当前线程注册到 Poller 并 yield
Poller.register(fd, Net.POLLIN);
Continuation.yield(SCOPE);
// -------------------------------------------------------------
// 【当重新 Thaw 恢复执行后,PC 指向此处,进入下一次循环再次发起 read】
// -------------------------------------------------------------
continue;
}
// 2. 再次执行 read 时,Linux Socket 接收缓冲区已有数据,sys_read 返回读取的字节数 n
return IOStatus.normalize(n);
}
}
六、 完整唤醒链路时序图汇总
下图归纳了从内核收到数据包到 Java 逻辑执行恢复的端到端调用序列:
| 序号 | 步骤 | 执行主体 | 运行空间 | 核心 API / 函数 | 关键动作 |
|---|---|---|---|---|---|
| 1 | 网卡中断 | Hardware / SoftIRQ | 内核态 | tcp_v4_rcv |
协议栈重组数据存入 sk_receive_queue |
| 2 | 链表唤醒 | Socket Subsystem | 内核态 | sock_def_readable → \to → ep_poll_callback |
将 epitem 加入 rdllist,唤醒阻塞在 epoll_wait 的线程 |
| 3 | Syscall 返回 | JVM Poller 线程 | 用户态 | Java_sun_nio_ch_EPoll_wait |
epoll_wait 接收到就绪的 fd 数量 |
| 4 | 映射 Thread | Poller Java 逻辑 | 用户态 | Poller.polled() |
从 fd 找到对应的 Poller.Request 和 VirtualThread |
| 5 | 状态 CAS | VirtualThread | 用户态 | VirtualThread.unpark() |
CAS 将虚拟线程状态从 PARKED 转换为 RUNNABLE |
| 6 | 压入队列 | Poller Java 逻辑 | 用户态 | ForkJoinPool.externalPush() |
将 runContinuation 压入 ForkJoinPool 无锁 Submission 队列 |
| 7 | 唤醒 Carrier | ForkJoinPool | 用户态 / 内核态 | LockSupport.unpark(Carrier) |
执行 sys_futex(FUTEX_WAKE) 唤醒休眠的 ForkJoin Worker 线程 |
| 8 | Mount/Thaw | Carrier 线程 | 用户态 (C++) | Continuation::thaw() |
从 stackChunkOop 恢复栈帧写回 Carrier 物理栈,重置 RSP/RBP |
| 9 | 续放执行 | VirtualThread | 用户态 (Java) | SocketChannelImpl.read() |
跳转至挂起时的 PC 继续运行,重新发起 sys_read 成功拿到数据 |