核心: Continuation
虚拟线程在遇到需要等待的操作时(如 future.get()、socket.read())会自动释放平台线程,并在等待的操作完成后继续运行。这一机制的核心就是 Continuation。
Continuation 的源码位于 Continuation.java,主要提供两个方法:
Continuation.yield(scope)
:挂起当前 continuationcontinuation.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 与虚拟线程的兼容主要需要解决两个问题:
- 在发起 IO 请求后挂起虚拟线程
- 在有数据到达时唤醒虚拟线程
基本思路是采用 IO 多路复用机制,工作流程如下:
- 发起 IO 请求的线程将需要等待的文件描述符 fd 注册到 epoll 中,然后挂起
- 主线程通过 epoll 监听事件
- 主线程收到 fd 的 IO 事件后,找到关联的虚拟线程并唤醒
- 虚拟线程从挂起点继续执行,此时 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);
}
}