Virtual Thread重投递至ForkJoinPool任务队列过程解析

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))
    • [二、 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))
    • [三、 `VirtualThread` 状态机变更与 Continuation 恢复提交](#三、 VirtualThread 状态机变更与 Continuation 恢复提交)
      • [1. 虚拟线程原子状态机 (`VirtualThread.java`)](#1. 虚拟线程原子状态机 (VirtualThread.java))
    • [四、 `ForkJoinPool` 外部任务入队与 Carrier 线程唤醒](#四、 ForkJoinPool 外部任务入队与 Carrier 线程唤醒)
      • [1. 外部入队与环形缓冲区操作 (`ForkJoinPool.java`)](#1. 外部入队与环形缓冲区操作 (ForkJoinPool.java))
      • [2. 唤醒空闲 Carrier 线程 (`signalWork`)](#2. 唤醒空闲 Carrier 线程 (signalWork))
    • [五、 Carrier 线程挂载 (Mount) 与 Native 栈帧解冻 (Thaw)](#五、 Carrier 线程挂载 (Mount) 与 Native 栈帧解冻 (Thaw))
      • [1. 唤醒后重新执行非阻塞 Syscall (`SocketChannelImpl.java`)](#1. 唤醒后重新执行非阻塞 Syscall (SocketChannelImpl.java))
    • [六、 完整唤醒链路时序图汇总](#六、 完整唤醒链路时序图汇总)

前言

本文旨在记录近期研读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 成功拿到数据
相关推荐
xuhe21 小时前
Codex解决新模型无法使用/model选择的问题
linux·ai·codex
小蒜学长1 小时前
在线保险服务与管理平台的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·服务平台·在线保险
All for pursuit.1 小时前
【回溯-6】79.单词搜索
数据结构·c++·算法·leetcode
掉进电商坑三年没爬出来的东叔1 小时前
青龙面板进阶:一个面板聚合多平台签到,所有积分自动领
java·开发语言
Navigator_Z1 小时前
LeetCode //C - 1281. Subtract the Product and Sum of Digits of an Integer
c语言·算法·leetcode
iCxhust1 小时前
51单片机电力载波应用开发(七)广播通讯
c语言·开发语言·单片机·嵌入式硬件·51单片机
well06121 小时前
Linux 文件相关底层知识
linux·运维·服务器
Benny_Tang2 小时前
AT_arc180_d [ARC180D] Division into 3 题解
数据结构·c++·算法
羔羊++2 小时前
26_实验二十五_busybox构建根文件系统
linux