Java 19推出了新特性“虚拟线程”,类似于Go语言中的协程。它是传统线程的不同之处在于,它是一种用户模式(user-mode)的线程。
虚拟线程是由 JDK 而非操作系统提供的线程的轻量级实现:
虚拟线程是没有绑定到特定操作系统线程的线程。
平台线程是以传统方式实现的线程,作为围绕操作系统线程的简单包装。
下面让我们实际开发一个多线程的程序,分别使用传统线程和虚拟线程,再来对比一下它们运行期间的性能表现。
该程序会创建10万个线程,每个线程并发运行(先睡眠一秒,再打印一个数字),运行完之后打印运行时间
传统线程程序
public class OSThreadMain {
public static void main(String[] args) throws InterruptedException, IOException {
TimeUnit.SECONDS.sleep(60);
AtomicInteger count = new AtomicInteger();
long start = System.currentTimeMillis();
try (var executor = Executors.newCachedThreadPool()) {
IntStream.range(0, 100000).forEach(i -> {
executor.submit(() -> {
System.out.println("参数" + (count.getAndIncrement()));
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
}
System.out.println("耗时" + (System.currentTimeMillis() - start));
System.in.read();
}
}
该代码中使用了线程池。
运行结果
可见整体运行时间达到了38秒多。
同时我用JConsole连接了程序,得到的监控图像
可以看出,整个程序运行过程中,比较突出的资源使用是线程数和CPU占用率,线程数达到了将近4000个线程,并持续了程序运行的大部分有效时间(前面为了能让JConsole能先连接上程序再进行监测,程序首先睡了60秒)。同时CPU的占用率也在程序有效运行时间内保持在30%到40%之间。
下面再看虚拟线程代码
public class VirtualThreadMain {
public static void main(String[] args) throws IOException, InterruptedException {
TimeUnit.SECONDS.sleep(60);
AtomicInteger count = new AtomicInteger();
long start = System.currentTimeMillis();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 100000).forEach(i -> {
executor.submit(() -> {
System.out.println("参数" + (count.getAndIncrement()));
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
}
System.out.println("耗时" + (System.currentTimeMillis() - start));
System.in.read();
}
}
除了使用了newVirtualThreadPerTaskExecutor方法代替newCachedThreadPool方法之外,代码完全相同。
运行结果
程序运行只花了4323毫秒,仅为传统线程的九分之一。再看JConsole监控结果
可见线程数仅为21个左右,而CPU占用率也仅是在一瞬间触达33%左右即迅速回落。可见在线程数和CPU占用率上,虚拟线程大大优于传统线程。
不过值得一提的是,从上两图可看出,传统线程内存占用仅150MB左右,而虚拟线程则为300MB左右,有两倍的差异。这是因为虚拟线程是JDK在用户模式实现,所以需要更复杂的数据结构去实现,而传统线程,则依赖于操作系统,所以JVM的内存占用就少了。