JDK20已经发布,搭载了第二轮虚拟线程preview。目前的JEP 444 中,已经去掉了 preview 标识,有望在下一个LTS版本JDK21中发布,发布日期为2023-09. 最新的虚拟线程将支持ThreadLocal.
- 概述 jdk19 已经于2022-09-20 正式发布,下载地址:https://jdk.java.net/19/ ,包含以下特性:
- 405: Record Patterns (Preview)
- 422: Linux/RISC-V Port
- 424: Foreign Function & Memory API (Preview)
- 425: Virtual Threads (Preview)
- 426: Vector API (Fourth Incubator)
- 427: Pattern Matching for switch (Third Preview)
- 428: Structured Concurrency (Incubator)
其中最令人激动的两个特性就是project loom项目带来的 JEP 425 虚拟线程(Virtual Threads) 和 JEP 428 结构化并发(Structured Concurrency).oracle 官方对于project loom非常看重,jdk 并发库作者、并发专家 brain goetz 也在采访中谈到,”project loom 将会杀死响应式编程”。
本文将着重介绍 虚拟线程(Virtual Thread).
- 虚拟线程
虚拟线程类似于协程,是一种用户态线程,由编程语言来管理与系统线程的映射关系。类似于go语言和kotlin的协程,但是比它们更强大和简洁。
实际上,java 曾经在非常古老的版本中就已经有用户态线程了,当时称为绿色线程,但它是M:1模型,即所有的绿色线程之对应一个平台线程。这种方式很快被全量使用平台线程的方式取代了。直到现在,project loom 带了新的调度器和新的工具类Continuation从而能支持M:N模型的用户态线程。
该图可以展示出平台线程(carrier thread) 和 虚拟线程(virtual thread)的关系,每当虚拟线程blocked或者waiting时,调度器会自动调度平台线程去承接其他虚拟线程的工作.
virutal thread 由 Continuation 和 Scheduler 组成:
Continuation:负责调整虚拟线程的状态,运行还是让出; yield方法可以让出对应的平台线程,run方法可以分配分配一个平台线程,继续运行 一般情况下,开发者不会直接接触该API,该api由虚拟线程封装。
该类定义如下:
package java.lang;
public class Continuation implements Runnable {
public Continuation(ContinuationScope scope, Runnable target)
public final void run()
public static void yield(ContinuationScope scope)
public boolean isDone() : }
scheduler 则由新的fork-join pool 承担,负责线程的调度工作;该pool并非fork-join poll的 common pool ,而是一个新的独立的pool 下面的例子会演示它们是两个独立的pool
创建虚拟线程 虚拟线程基于 Thread 类新增了一些方法,这些方法可以方便地创建平台线程或者虚拟线程。jdk 并没有提供一个新的线程类,所以这方面的兼容性非常好。
Thread.ofPlatfrom() // 生成平台线程,也就是我们现在用的线程
Thread.ofVirtual() // 生成虚拟线程
特性 thread 提供的方法已经在内部实现了对于平台线程和虚拟线程的兼容,比如下面的sleep,当检测到当前线程是虚拟线程时,最终sleep的逻辑的就是调用Continuation.yield()来实现sleep;
public static void sleep(long millis) throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (currentThread() instanceof VirtualThread vthread) {
long nanos = MILLISECONDS.toNanos(millis);
vthread.sleepNanos(nanos);
return;
}
if (ThreadSleepEvent.isTurnedOn()) {
ThreadSleepEvent event = new ThreadSleepEvent();
try {
event.time = MILLISECONDS.toNanos(millis);
event.begin();
sleep0(millis);
} finally {
event.commit();
}
} else {
sleep0(millis);
}
}
虚拟线程的阻塞代价非常低,你可以放心地去阻塞虚拟线程。当虚拟线程被阻塞时,会把当前虚拟线程所对应的stack 导出到堆中,等待阻塞结束,可以运行时,又从内存中讲其恢复,恢复时,可能会切换到另外一个虚拟线程。这个过程中,可以保证平台线程始终在做不阻塞的任务,从而提升平台线程的利用率;
这里将创建1百万个线程,每个线程休眠10s,使用平台线程和虚拟线程做对比:
1m thread sleep test
使用平台线程时,很快就因为占用的内存过多而导致oom
更换为虚拟线程
成功运行了 一百万次
不同的fork-join pool
public class PoolName {
public static void main(String[] args) {
ForkJoinPool.commonPool().execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread());
}
});
try {
Thread.ofVirtual().start(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread());
}
}).join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
Thread[#21,ForkJoinPool.commonPool-worker-1,5,main]
VirtualThread[#22]/runnable@ForkJoinPool-1-worker-1
使用虚拟线程,你可以放心大胆地使用上百万的线程完成工作,不需要担心它们使用过多的平台线程资源。下面的例子将会展示使用1千万的虚拟线程,最终只使用了7个平台线程。
public static void main(String[] args) {
AtomicInteger ai = new AtomicInteger();
Set<String> set = new HashSet<>();
for(int i = 0;i<10_000_000;i++){
try {
Thread.ofVirtual().start(new Runnable() {
@Override
public void run() {
try {
set.add(realWorkerName(Thread.currentThread().toString()));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}).join();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
System.out.println(set.size());
System.out.println(set);
}
private static String realWorkerName(String threadName){
return threadName.substring(threadName.indexOf("@"),threadName.length());
}
运行结果
7
[@ForkJoinPool-1-worker-6, @ForkJoinPool-1-worker-7, @ForkJoinPool-1-worker-4, @ForkJoinPool-1-worker-5, @ForkJoinPool-1-worker-2, @ForkJoinPool-1-worker-3, @ForkJoinPool-1-worker-1]
在笔者的mbp m1 (8c 16g)上,只需要使用7个平台线程即可支撑1千万的虚拟线程,这就是虚拟线程的强大.
io 优化 jdk修改了所有bio阻塞通信的地方;运行在虚拟线程环境中,当代码阻塞时(和Thread.sleep()一样的原理,针对虚拟线程做了兼容),它会将当前的状态保存到内存中,当代码阻塞结束时,会自动唤醒,并重新运行(可能会切换到另外一个虚拟线程)
vthread 和 pthread 的关系
关于 虚拟线程,更多的信息可以到 JEP 了解: https://openjdk.org/jeps/425
- 总结 虚拟线程拥有以下特性: 很大程度地提升java程序的吞吐量(并不会变快) 完全兼容原有Thread类api,现有程序的变更成本较小 轻量级线程扩展,阻塞成本足够低,可以一次性运行上千万的虚拟线程 以同步方式编写异步代码(甚至不会出现满地的async、await,而C#和kotlin有),降低异步成的开发、调试成本
虚拟线程目前还是preview的状态,到正式发布可能还需要几个jdk版本迭代,期望未来的JDK21就能搭载正式版本的虚拟线程,这将彻底改变java的并发编程.