虚拟线程内部实现

核心: Continuation

虚拟线程在遇到需要等待的操作时(如 future.get()、socket.read())会自动释放平台线程,并在等待的操作完成后继续运行。这一机制的核心就是 Continuation。

Continuation 的源码位于 Continuation.java,主要提供两个方法:

  • Continuation.yield(scope):挂起当前 continuation
  • continuation.run():从挂起处继续执行该 continuation

通过以下代码可以理解 continuation 的工作流程:

java 复制代码
import jdk.internal.vm.Continuation;
import jdk.internal.vm.ContinuationScope;

public class ContinuationDemo {
    public static void main(String[] args) {
        ContinuationScope scope = new ContinuationScope("DemoScope");
        
        Continuation continuation = new Continuation(scope, () -> {
            System.out.println("1");
            Continuation.yield(scope);
            System.out.println("2");
            Continuation.yield(scope);
            System.out.println("3");
        });
        
        // 第一次运行
        continuation.run();
        System.out.println("do other things 1");
        
        // 第二次运行(从上次yield处继续)
        continuation.run();
        System.out.println("do other things 2");
        
        // 第三次运行
        continuation.run();
        System.out.println("finished");
        
        // 已终止,会报错
        continuation.run();
    }
}

执行结果如下:

shell 复制代码
$ java --add-exports java.base/jdk.internal.vm=ALL-UNNAMED ContinuationDemo.java 
1
do other things 1
2
do other things 2
3
finished
Exception in thread "main" java.lang.IllegalStateException: Continuation terminated
        at java.base/jdk.internal.vm.Continuation.run(Continuation.java:238)
        at ContinuationDemo.main(ContinuationDemo.java:29)

可以看到,每次调用 yield 都会返回到 run 的下一句,而每次调用 run 都会从 yield 的下一句继续执行。Continuation 主要由 JVM 实现。

java.util.concurrent 如何兼容虚拟线程?

以下面的代码中的 lock.lock() 为例。当无法获取锁时,如果是在虚拟线程中执行,就会挂起并释放平台线程;如果是在普通线程中执行,则会挂起平台线程并等待唤醒。

java 复制代码
class X {
    private final ReentrantLock lock = new ReentrantLock();

    public void m() {
        lock.lock();  // block until condition holds
        try {
          // ... method body
        } finally {
          lock.unlock()
        }
    }
}

这是如何实现的呢?关键在于 LockSupport.park()/unpark()。concurrent 包里的大部分并发数据结构的底层都是 AbstractQueuedSynchronizer。在 AbstractQueuedSynchronizer 中,当线程需要等待时会调用 LockSupport.park(thread) 挂起线程,需要唤醒时则调用 LockSupport.unpark(thread)

查看 LockSupport 的具体实现,可以发现当当前线程是虚拟线程时,会进入 VirtualThreads.park()

java 复制代码
public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    try {
        if (t.isVirtual()) {
            VirtualThreads.park();
        } else {
            U.park(false, 0L);
        }
    } finally {
        setBlocker(t, null);
    }
}

VirtualThreads.park() 最终会调用 VirtualThread.park(), 其中会调用 yieldContinuation(),进而调用Continuation.yield(VTHREAD_SCOPE)。具体代码位于 VirtualThread.java#L441

类似地,VirtualThread.unpark() 会调用 submitRunContinuation(),并最终由 scheduler 调用 cont.run()。代码位于 VirtualThread.java#L223。默认的 scheduler 是一个 ForkJoinPool.

综上所述,通过在 LockSupport.park()/unpark() 中实现不同的代码路径,就实现了 concurrent 包与虚拟线程的兼容。

Socket IO 如何兼容虚拟线程?

Socket IO 与虚拟线程的兼容主要需要解决两个问题:

  1. 在发起 IO 请求后挂起虚拟线程
  2. 在有数据到达时唤醒虚拟线程

基本思路是采用 IO 多路复用机制,工作流程如下:

  1. 发起 IO 请求的线程将需要等待的文件描述符 fd 注册到 epoll 中,然后挂起
  2. 主线程通过 epoll 监听事件
  3. 主线程收到 fd 的 IO 事件后,找到关联的虚拟线程并唤醒
  4. 虚拟线程从挂起点继续执行,此时 fd 已可读取,只需完成实际读取操作

接下来看具体的代码实现,以 NioSocketImpl.java 为例。

read 方法会调用 park 来注册监听并挂起自己。park 方法中会调用 Poller.poll()

java 复制代码
private void park(FileDescriptor fd, int event, long nanos) throws IOException {
    Thread t = Thread.currentThread();
    if (t.isVirtual()) {
        Poller.poll(fdVal(fd), event, nanos, this::isOpen);
        if (t.isInterrupted()) {
            throw new InterruptedIOException();
        }
    } else {
        // ...
    }
}

Poller.poll() 里会调用 pollDirect。该方法会先调用 register(fdVal) 注册当前线程监听 fdVal,然后调用 LockSupport.park() 挂起虚拟线程。

java 复制代码
private void pollDirect(int fdVal, long nanos, BooleanSupplier supplier) throws IOException {
    register(fdVal);
    try {
        boolean isOpen = supplier.getAsBoolean();
        if (isOpen) {
            if (nanos > 0) {
                LockSupport.parkNanos(nanos);
            } else {
                LockSupport.park();
            }
        }
    } finally {
        deregister(fdVal);
    }
}

register 方法先将当前线程与 fdVal 存入 map,这是为了之后当 fdVal 可以读取时找到需要唤起的线程。然后调用 implRegister(fdVal)

java 复制代码
private void register(int fdVal) throws IOException {
    Thread previous = map.putIfAbsent(fdVal, Thread.currentThread());
    assert previous == null;
    implRegister(fdVal);
}

implRegister 在不同的平台里有不同的实现,Linux 平台通过 epoll 实现,具体是通过调用 epoll_ctl 完成注册:

java 复制代码
void implRegister(int fdVal) throws IOException {
    // re-arm
    int err = EPoll.ctl(epfd, EPOLL_CTL_MOD, fdVal, (event | EPOLLONESHOT));
    if (err == ENOENT)
        err = EPoll.ctl(epfd, EPOLL_CTL_ADD, fdVal, (event | EPOLLONESHOT));
    if (err != 0)
        throw new IOException("epoll_ctl failed: " + err);
}

主线程通过 epoll_wait 获取就绪的 fdVal,然后调用 polled(fdVal) 找到对应虚拟线程并通过 LockSupport.unpark() 唤醒。

java 复制代码
int poll(int timeout) throws IOException {
    int n = EPoll.wait(epfd, address, MAX_EVENTS_TO_POLL, timeout);
    int i = 0;
    while (i < n) {
        long eventAddress = EPoll.getEvent(address, i);
        int fdVal = EPoll.getDescriptor(eventAddress);
        polled(fdVal);
        i++;
    }
    return n;
}

final void polled(int fdVal) {
    wakeup(fdVal);
}

private void wakeup(int fdVal) {
    Thread t = map.remove(fdVal);
    if (t != null) {
        LockSupport.unpark(t);
    }
}

阅读更多

相关推荐
考虑考虑12 分钟前
使用jpa中的group by返回一个数组对象
spring boot·后端·spring
GiraKoo20 分钟前
【GiraKoo】C++11的新特性
c++·后端
MO2T25 分钟前
使用 Flask 构建基于 Dify 的企业资金投向与客户分类评估系统
后端·python·语言模型·flask
云动雨颤32 分钟前
Java并发性能优化|读写锁与互斥锁解析
java
光溯星河33 分钟前
【实践手记】Git重写已提交代码历史信息
后端·github
ldj20201 小时前
Centos 安装Jenkins
java·linux
PetterHillWater1 小时前
Trae中实现OOP原则工程重构
后端·aigc
hqxstudying1 小时前
Intellij IDEA中Maven的使用
java·maven·intellij-idea
圆滚滚肉肉1 小时前
后端MVC(控制器与动作方法的关系)
后端·c#·asp.net·mvc
SimonKing1 小时前
拯救大文件上传:一文彻底彻底搞懂秒传、断点续传以及分片上传
java·后端·架构