Java Virtual Thread Java虚拟线程
Java 21 (LTS) 这个长期支持版本引入了Java虚拟线程。之前版本的线程是与os的线程一一对应的,现在称为平台线程 Platform Thread。新引入的虚拟线程 Virtual Thread是可以比喻为一个任务,它会绑定在某一个平台线程上执行,当遇到阻塞io或是sleep等函数时,会从平台线程解绑,允许其他的虚拟线程任务使用这个平台线程继续执行。
这种情况下,可以使用少量的平台线程执行大量的虚拟线程任务,即使虚拟线程任务中会出现长时间的io等待。
虚拟线程的实现原理是改变了很多原jdk内部代码的实现。以Thread.sleep()
函数举例,其内部实现是主动调用java.lang.VirtualThread#unmount
函数,主动从平台线程解绑,让出CPU。
如果虚拟线程内部执行了纯CPU的计算过程,或是一个单纯的死循环,或是使用synchronized
关键字,这些都不会主动调用解绑函数,这个虚拟线程任务将一直占用这个平台线程,也就是OS线程,其他的虚拟线程任务则无法继续执行。这一点与系统的原生线程不同,系统线程是会根据时间片进行调度的,如果某个系统线程使用完一整个时间片,他也会让出CPU,允许其它的系统线程继续执行。而Java的虚拟线程不同,不主动调用解绑函数,则会一直占用系统线程。
风险点
线程的java api中没有任何办法指定Java的虚拟线程可以使用的平台线程池。
即在调用java.lang.VirtualThread#VirtualThread
函数时,无法传入指定的Executor scheduler
例如如下的VirtualThreadBuilder构造函数,注释中写着仅用于测试
// invoked by tests
VirtualThreadBuilder(Executor scheduler) {
if (!ContinuationSupport.isSupported())
throw new UnsupportedOperationException();
this.scheduler = Objects.requireNonNull(scheduler);
}
现在Java的虚拟线程只允许绑定在java.lang.VirtualThread#DEFAULT_SCHEDULER
这一个平台线程池中,所有的虚拟线程任务都使用这一个平台线程池,该线程池默认最大创建当前CPU核数的线程。
如果在一个4核的CPU机器上执行了4个内部是单纯死循环的虚拟线程任务,则其他的虚拟线程任务再也无法执行了。我们还无法通过指定平台线程池来隔离各类虚拟线程任务的执行绑定。因此风险还挺大的,如果使用虚拟线程的业务逻辑内出现bug持续占用系统线程,后果还挺严重。
使用如下示例代码可以验证。每个虚拟线程任务都在执行一段长耗时的纯CPU计算任务。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 0; i < 10; i++) {
String num = String.valueOf(i);
executorService.execute(() -> doWork(num));
}
try {
executorService.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private static void doWork(String s) {
long start = System.currentTimeMillis();
List<String> threadList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
threadList.add(Thread.currentThread().toString());
computeIntensiveOperation();
}
long duration = System.currentTimeMillis() - start;
boolean allSame = true;
for (String string : threadList) {
if (!string.equals(threadList.get(0))) {
allSame = false;
}
}
System.out.println("work " + s + " duration:" + duration + "ms allSamePlatformThread:" + allSame + " " + threadList);
}
private static double computeIntensiveOperation() {
double sum = 0;
// 复杂的数学运算,增加计算量
for (int i = 1; i <= 2000000; i++) {
sum += Math.sqrt(i) * Math.sin(i) - Math.log(i);
}
return sum;
}
}
打印出的示例日志如下
work 0 duration:11348ms allSamePlatformThread:true [VirtualThread[#23]/runnable@ForkJoinPool-1-worker-1, ...
work 3 duration:11348ms allSamePlatformThread:true [VirtualThread[#27]/runnable@ForkJoinPool-1-worker-4, ...
work 2 duration:11430ms allSamePlatformThread:true [VirtualThread[#26]/runnable@ForkJoinPool-1-worker-3, ...
work 1 duration:11439ms allSamePlatformThread:true [VirtualThread[#25]/runnable@ForkJoinPool-1-worker-2, ...
work 4 duration:10880ms allSamePlatformThread:true [VirtualThread[#28]/runnable@ForkJoinPool-1-worker-4, ...
work 6 duration:10827ms allSamePlatformThread:true [VirtualThread[#30]/runnable@ForkJoinPool-1-worker-3, ...
work 5 duration:10908ms allSamePlatformThread:true [VirtualThread[#29]/runnable@ForkJoinPool-1-worker-1, ...
work 7 duration:10905ms allSamePlatformThread:true [VirtualThread[#31]/runnable@ForkJoinPool-1-worker-2, ...
work 8 duration:10355ms allSamePlatformThread:true [VirtualThread[#32]/runnable@ForkJoinPool-1-worker-4, ...
work 9 duration:10359ms allSamePlatformThread:true [VirtualThread[#33]/runnable@ForkJoinPool-1-worker-3, ...
代码通过Thread.currentThread().toString()
可以获取出当前虚拟线程的名称以及绑定执行的平台线程名称。通过最终打印出的日志可以发现每个虚拟线程任务的整个执行过程都在一个平台线程上执行,没有像OS系统线程一样的时间片调度过程。在一个四核的机器上执行,也能每次完成四个虚拟线程任务,然后隔一段时间后再完成四个虚拟线程任务,虚拟线程任务直接不会交错执行。
注意不能在CPU密集运算过程中穿插System.out.println()
函数,System.out.println()
函数会主动解除平台线程的绑定,无法实现我们想要验证的效果。所以我们选择在CPU密集运算过程中将线程名称放入一个list中,运算完毕后统一打印。