贝壳面试题:JDK21中的虚拟线程是怎么回事?

46 阅读5分钟

文章内容收录到个人网站,方便阅读:hardyfish.top/

资料分享

Java虚拟机规范.Java SE 8版:

深入理解Java虚拟机:JVM高级特性与最佳实践(第3版):

提到“虚拟线程”,很多人可能会觉得陌生,但如果你了解过像Go、Ruby或Python这样的语言,可能马上会联想到“协程”。

没错,JDK 21引入的虚拟线程(Virtual Thread),本质上就是Java版的协程,旨在解决传统线程模型的性能瓶颈。

传统Java线程的问题

在JDK 21之前,Java的线程模型比较简单,采用的是“一对一”的映射方式:每个Java线程对应操作系统的一个轻量级进程(通常是内核线程)。

这种模型下,线程的创建、销毁和同步操作都需要系统调用,而系统调用需要在用户态和内核态之间切换,开销不小。

尤其是在高并发场景下,大量线程频繁切换上下文,会显著影响性能。

此外,由于线程数受限于操作系统和硬件资源,创建过多线程可能会耗尽系统资源。

虚拟线程的改进

虚拟线程是JDK实现的轻量级线程,核心变化在于它打破了“一对一”的映射关系。

JDK不再为每个线程分配一个独立的操作系统线程,而是将多个虚拟线程映射到少量的操作系统线程上,通过JVM内部的调度机制管理这些虚拟线程。这样就避免了昂贵的上下文切换开销。

虚拟线程的另一个亮点是数量几乎不受限。你可以创建成千上万的虚拟线程,而不用担心平台线程的数量限制。

因为它们本质上是JVM管理的普通Java对象,存储在内存中,开销极低。

线程实现的背景知识

为了更好地理解虚拟线程,我们先看看操作系统的线程实现方式,主要有三种:

  1. 内核线程实现:线程直接由操作系统内核管理,调度和创建都依赖内核,常见于Windows和Linux。
  2. 用户线程实现:线程由用户态的库管理,内核不可见,开销低但无法利用多核CPU。
  3. 混合实现:用户线程和内核线程结合,比如Solaris的模型,既能利用多核,又能减轻内核负担。

Java作为跨平台语言,它的线程实现依赖底层操作系统。在常见的Windows和Linux上,Java线程传统上是基于内核线程的“一对一”模型。这种方式虽然稳定,但由于每次创建和调度都需要内核参与,成本较高。即使引入线程池来复用线程,优化空间依然有限。

虚拟线程的特点与区别

虚拟线程和传统平台线程(Platform Thread)有几个显著区别:

  1. 守护线程特性:虚拟线程默认是守护线程,无法通过setDaemon(false)改为非守护线程。这意味着JVM不会等待虚拟线程执行完毕就退出,开发者需要注意这一点。
  2. 优先级固定:虚拟线程优先级固定为“normal”,调用setPriority()无效。
  3. 不支持老接口:像stop()suspend()resume()这些过时方法在虚拟线程上会抛出异常。

这些设计选择反映了虚拟线程的轻量本质,避免了复杂的状态管理。

如何使用虚拟线程?

JDK 21提供了多种创建虚拟线程的方式:

  1. 直接启动 使用Thread.startVirtualThread()运行一个任务:

    Thread.startVirtualThread(() -> System.out.println("Hello from virtual thread!"));
    
  2. Thread.Builder 通过Thread.ofVirtual()创建虚拟线程(相对的,ofPlatform()创建平台线程):

    Thread virtual = Thread.ofVirtual().start(() -> System.out.println("Virtual thread"));
    
  3. ExecutorService 使用Executors.newVirtualThreadPerTaskExecutor()创建虚拟线程执行器:

    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        executor.submit(() -> System.out.println("Task in virtual thread"));
    }
    

不过有个建议:尽量别把虚拟线程和线程池混用。

线程池是为复用昂贵的平台线程设计的,但虚拟线程创建成本低,池化反而多此一举,可能限制其灵活性。

性能对比

虚拟线程到底有多大改进?我们可以用一个简单实验来看看。

假设有个任务:等待1秒后打印消息,模拟I/O阻塞。我们测试10,000个任务的执行时间。

平台线程实现

public static void testPlatformThreads(int taskCount) throws InterruptedException {
    long start = System.nanoTime();
    Thread[] threads = new Thread[taskCount];
    for (int i = 0; i < taskCount; i++) {
        threads[i] = new Thread(() -> {
            try {
                Thread.sleep(1000);
                System.out.println("Task done");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        threads[i].start();
    }
    for (Thread t : threads) t.join();
    long end = System.nanoTime();
    System.out.println("Platform Threads Time: " + (end - start) / 1_000_000 + " ms");
}

运行结果:大约1000ms。

虚拟线程实现

public static void testVirtualThreads(int taskCount) throws InterruptedException {
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        long start = System.nanoTime();
        for (int i = 0; i < taskCount; i++) {
            executor.submit(() -> {
                try {
                    Thread.sleep(1000);
                    System.out.println("Task done");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
        long end = System.nanoTime();
        System.out.println("Virtual Threads Time: " + (end - start) / 1_000_000 + " ms");
    }
}

运行结果:大约200ms。

从1000ms到200ms,性能提升非常明显。虚拟线程通过高效调度,避免了大量线程切换的开销,尤其在高并发I/O场景下优势显著。

总结

JDK 21的虚拟线程是Java线程模型的一次重大升级。它通过将多个轻量级虚拟线程映射到少量操作系统线程,极大降低了上下文切换成本,同时支持高并发任务。

相比传统平台线程,虚拟线程更灵活、开销更低,非常适合现代高并发应用。

不过,它也有局限性,比如守护线程特性和不支持某些老接口,使用时需要调整设计思路。

总的来说,虚拟线程让Java在协程领域迈出了重要一步,带来了显著的性能提升。