一、前言
JDK21新增了“虚拟线程”的新特性,但在实际探索过程中,发现虚拟线程目前还不够成熟,存在一些缺陷( 官方文档:JEP 444: Virtual Threads)。本文列举了这些缺陷,并提供了可以触发这些缺陷的示例。
二、缺陷及示例
1、使用虚拟线程后无法通过池化的方式、配置最大线程数来控制并发数,但可以通过信号量等方法来专门解决这个问题
2、JDK中的一些阻塞操作不会卸载虚拟线程,从而阻塞平台线程和底层OS线程,导致载体线程暂时扩容、产生性能开销,示例如下:
public class CarrierThreadExpandTest {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
File file = new File("/Users/info.log");
for (int i = 0; i < 32; i++) {
executorService.execute(() -> {
while (true) {
try (FileInputStream fis = new FileInputStream(file)) {
int content;
while ((content = fis.read()) != -1) {
System.out.print((char) content);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
Thread.sleep(1000 * 60 * 60);
}
}
通过jstack pid 指令查看"ForkJoinPool-1-worker-xx”线程即为平台线程,可以看到发生了扩容。
3、在 synchronized 块或方法中执行代码、执行 native 方法或外部函数时,虚拟线程发生阻塞不仅无法被卸载(pinned),载体线程也不会扩容;会导致其他虚拟线程得不到调度,示例如下:
public class VirtualThreadUnmountTest {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 0; i < 8; i++) {
executorService.execute(VirtualThreadUnmountTest::doSth);
}
long[] nums = new long[10];
for (int i = 0; i < 10; i++) {
int finalI = i;
executorService.execute(() -> {
while (true) {
nums[finalI]++;
}
});
}
Thread.sleep(1000 * 15);
System.out.println(Arrays.toString(nums));
}
public synchronized static void doSth() {
try {
Thread.sleep(1000 * 60 * 60);
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
}
}
// 输出:[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
4、虚拟线程暂未实现基于时间分片的抢占式调度(对于传统的平台线程,这部分工作由 OS 负责完成);如果部分任务不阻塞地持续运行,就会持续占用CPU,影响其他任务执行(正常情况下我们的业务线程应该不会出现持续运行的情况),示例如下:
public class VirtualThreadHoldCpuTest {
public static void main(String[] args) throws InterruptedException {
// newVirtualThreadPerTaskExecutor,为每一个请求创建一个虚拟线程
ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
long[] nums = new long[100];
for (int i = 0; i < 100; i++) {
int finalI = i;
executorService.execute(() -> {
// 死循环持续运行
while (true) {
nums[finalI]++;
}
});
}
Thread.sleep(1000 * 30);
System.out.println(Arrays.toString(nums));
}
}
// 输出:[7021009361, 6579258416, 6695810714, 6931415415, 6924053995, 6984066224, 6900750367, 6839905353, 0, 0, 0, 0, 0, 0, 0, 0,
// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
5、结构化并发特性中断子任务是通过调用 Thread.interrupt 方法实现的,sleep、wait方法可以自动响应该中断;但是我们自己的业务代码可能会无法自动响应。