Java 21虚拟线程(Virtual Threads)深度解析

引言

2023年9月,Java 21正式发布,虚拟线程(Virtual Threads)作为JEP 444的正式特性落地。这不是一次普通的API更新,而是Java并发模型自1996年以来最重大的变革。

先看一个真实场景。某电商平台的订单查询服务,采用经典的"一请求一线程"模型,部署在8核16G的容器中,Tomcat最大线程数设置为200。日常QPS在3000左右运行良好,但每逢大促,流量涨到8000时,请求队列迅速堆积,大量请求超时。运维的第一反应是调大线程数到500,结果CPU上下文切换开销剧增,吞吐量不升反降,P99延迟从200ms飙到3秒。

这个困境的根源在于:平台线程(Platform Thread)是操作系统线程的一对一映射。每个线程占用约1MB栈空间,创建和切换需要陷入内核态,成本高昂。你无法在有限的物理资源上运行大量阻塞型任务。

传统的解法有两种:一是用异步编程(CompletableFuture、Reactor),把阻塞调用改造成回调链,但代码可读性断崖式下降,异常栈难以追踪;二是用协程框架(如Quasar、Kotlin Coroutines),但需要侵入式改造或更换语言。

虚拟线程给出了第三条路:保持同步阻塞的编程模型,却获得异步编程的吞吐量。这背后的实现机制,正是本文要深挖的核心。

核心概念:从餐厅后厨到虚拟线程

生活化类比

想象一家餐厅的后厨。

传统线程池模型:餐厅雇了200名厨师(平台线程),每人负责一道菜从头做到尾。当厨师需要等烤箱里的牛排(I/O阻塞)时,他就站在烤箱前干等,什么也做不了。高峰期订单暴增,200名厨师全都在等烤箱,新订单只能排队。你不能无限雇厨师,因为每个厨师都要占一个工位(内存),厨房站不下。

虚拟线程模型:餐厅只雇8名厨师(载体线程,等于CPU核数),但配备了10000个"订单牌"(虚拟线程)。当某个订单牌对应的菜需要等烤箱时,厨师把订单牌往墙上一挂(挂起虚拟线程),转手去做下一道能立刻推进的菜。烤箱"叮"的一声响(I/O完成),厨师取回订单牌继续做。厨师永远不闲着,订单牌的成本又极低(初始几百字节)。

关键洞察:厨师是稀缺资源(CPU),订单牌是廉价资源(内存)。虚拟线程的本质,就是让稀缺的厨师只在真正需要CPU时才工作,等待时间全部让给别人。

技术定义

虚拟线程是java.lang.Thread的一个实例,但它不绑定到操作系统线程。它由JVM调度,运行在少量的载体线程(Carrier Thread)上。当虚拟线程执行阻塞操作(如sleep、Socket读写、LockSupport.park)时,JVM会将其卸载(unmount),释放载体线程去执行其他虚拟线程;阻塞结束后再重新挂载(remount)。

// 创建虚拟线程的三种方式
Thread vThread1 = Thread.ofVirtual().start(() -> System.out.println("虚拟线程1"));

Thread vThread2 = Thread.ofVirtual()
    .name("order-query-", 0)
    .unstarted(() -> System.out.println("虚拟线程2"));
vThread2.start();

// 工厂方式
ThreadFactory factory = Thread.ofVirtual().factory();
Thread vThread3 = factory.newThread(() -> System.out.println("虚拟线程3"));
vThread3.start();

源码级原理深度分析

1. 虚拟线程的类层次结构

打开JDK 21的源码,Thread类中有一个关键字段:

// java.lang.Thread (JDK 21)
public class Thread implements Runnable {
    // 虚拟线程的实现在这里
    private final VirtualThread vthread;
    // 平台线程的实现在这里
    private final PlatformThread platformThread;
    
    // 判断是否为虚拟线程
    public final boolean isVirtual() {
        return vthread != null;
    }
}

Thread成为了一个"壳",真正的执行逻辑委托给VirtualThread或PlatformThread。这是JDK 19以来的重构,目的是让虚拟线程和平台线程共享Thread这个API入口,保证生态兼容。

2. 虚拟线程的调度核心:Continuation

虚拟线程的挂起/恢复能力,建立在jdk.internal.vm.Continuation之上。Continuation是"可暂停的计算单元",它封装了一段可以随时中断和恢复的代码执行状态。

// jdk.internal.vm.Continuation 简化示意
public class Continuation {
    private final Runnable target;       // 要执行的代码
    private StackChunk stackChunk;       // 保存的栈帧
    private boolean mounted;             // 是否正在运行
    
    // 挂起:把当前栈帧拷贝到堆上的stackChunk,然后让出载体线程
    public static void yield(ContinuationScope scope) {
        Continuation cont = currentContinuation(scope);
        if (cont != null) {
            cont.yield0();  // 底层是native方法,触发栈帧拷贝
        }
    }
    
    // 恢复:从stackChunk恢复栈帧,继续执行
    public void run() {
        // ...
    }
}

关键机制:栈帧的堆化(Stack Chunk)

平台线程的栈在操作系统的栈内存里,JVM无法搬动它。虚拟线程的栈是存储在Java堆上的StackChunk对象。当虚拟线程挂起时,JVM执行一次"栈帧拷贝"——把当前栈上的帧冻结到堆上的StackChunk中。恢复时反向操作。

这个操作听起来昂贵,但实际非常快,因为:

  • 栈帧拷贝是内存到内存的连续拷贝,不涉及内核态切换
  • 栈帧采用分片(chunk)结构,按需分配,初始只有几百字节
  • 相比线程上下文切换(微秒级),虚拟线程挂起是纳秒级

3. 调度器:ForkJoinPool的定制

虚拟线程的调度器是一个专用的ForkJoinPool,默认并行度等于CPU核数:

// java.lang.VirtualThread 中的调度器设置
private static final ForkJoinPool DEFAULT_SCHEDULER = createDefaultScheduler();

private static ForkJoinPool createDefaultScheduler() {
    int parallelism = Runtime.getRuntime().availableProcessors();
    // 关键配置:异步模式,不等待任务完成
    return new ForkJoinPool(parallelism, 
        ForkJoinPool.defaultForkJoinWorkerThreadFactory,
        null, true);  // asyncMode = true
}

载体线程就是ForkJoinPool的工作线程。当虚拟线程挂起时,它通知调度器"我让出了",调度器从任务队列取出下一个虚拟线程交给这个载体线程。

graph TD A[虚拟线程 vt-1] -->|提交| B[ForkJoinPool调度器] C[虚拟线程 vt-2] -->|提交| B D[虚拟线程 vt-N] -->|提交| B B -->|分配| E[载体线程 CT-1] B -->|分配| F[载体线程 CT-2] B -->|分配| G[载体线程 CT-M] E -->|挂载运行| A A -->|阻塞时卸载| E E -->|重新分配| C F -->|挂载运行| D G -->|挂载运行| C H[操作系统线程池] -.1:1映射.- E H -.1:1映射.- F H -.1:1映射.- G style A fill:#e1f5ff style C fill:#e1f5ff style D fill:#e1f5ff style E fill:#ffe1e1 style F fill:#ffe1e1 style G fill:#ffe1e1

4. 阻塞操作的拦截:谁在触发卸载?

虚拟线程能在阻塞时自动卸载,关键在于JDK核心库对阻塞点做了改造。以Thread.sleep为例:

// java.lang.Thread (JDK 21)
public static void sleep(long millis) throws InterruptedException {
    if (millis < 0) throw new IllegalArgumentException("timeout value is negative");
    if (currentThread() instanceof VirtualThread vthread) {
        // 虚拟线程路径:调用VirtualThread的sleep
        vthread.sleep(millis);
    } else {
        // 平台线程路径:原有实现
        sleep0(millis);
    }
}

进入VirtualThread.sleep后:

// java.lang.VirtualThread
void sleep(long millis) throws InterruptedException {
    // 使用JUC的定时器挂起,而不是OS的sleep
    if (Thread.currentThread() != this) throw new IllegalCallerException();
    if (millis < 0) throw new IllegalArgumentException();
    if (millis == 0) {
        // Thread.yield()的语义
        Thread.yield();
        return;
    }
    long nanos = MILLISECONDS.toNanos(millis);
    // 关键:通过park/unpark机制挂起
    try {
        ThreadSleepEvent event = ...;
        // 底层调用Continuation.yield,卸载虚拟线程
        parkNanos(nanos);
    } finally {
        // ...
    }
}

类似的改造遍布JDK:

阻塞操作 改造后的行为
Thread.sleep 挂起虚拟线程,定时器唤醒
Socket 读写 NIO Poller 注册事件,挂起等待
LockSupport.park 挂起虚拟线程,unpark时恢复
synchronized 不挂起(见避坑章节)
Object.wait 挂起虚拟线程
File I/O 不挂起(JDK 21限制)

5. 状态机:虚拟线程的生命周期

虚拟线程有比平台线程更复杂的状态机:

// java.lang.VirtualThread 内部状态
private static final int NEW = 0;
private static final int STARTED = 1;
private static final int RUNNING = 2;      // 已挂载到载体线程
private static final int PARKING = 3;      // 正在挂起
private static final int PARKED = 4;       // 已挂起
private static final int PINNED = 5;       // 被固定,无法卸载
private static final int YIELDING = 6;     // 主动让出
private static final int TERMINATED = 7;

PINNED状态是理解虚拟线程性能陷阱的关键:当虚拟线程在synchronized块内或执行native方法时,它无法被卸载,只能"钉"在载体线程上,此时载体线程被独占,失去了调度其他虚拟线程的能力。

实战代码

示例1:虚拟线程 vs 平台线程的吞吐量对比

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 对比虚拟线程与平台线程在处理阻塞任务时的吞吐量差异
 * 场景:模拟10000个任务,每个任务阻塞100ms(类似调用下游API)
 */
public class ThroughputComparison {

    private static final int TASK_COUNT = 10_000;
    private static final long BLOCK_MILLIS = 100;

    public static void main(String[] args) throws Exception {
        System.out.println("CPU核心数: " + Runtime.getRuntime().availableProcessors());
        
        // 方案一:固定大小平台线程池(模拟Tomcat 200线程)
        runWithPlatformThreads(200);
        
        // 方案二:虚拟线程(每任务一线程)
        runWithVirtualThreads();
        
        // 方案三:虚拟线程 + 信号量限制并发(防止下游被打爆)
        runWithVirtualThreadsAndSemaphore(500);
    }

    private static void runWithPlatformThreads(int poolSize) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(poolSize);
        long start = System.currentTimeMillis();
        
        CountDownLatch latch = new CountDownLatch(TASK_COUNT);
        for (int i = 0; i < TASK_COUNT; i++) {
            pool.submit(() -> {
                try {
                    Thread.sleep(BLOCK_MILLIS);  // 模拟I/O阻塞
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    latch.countDown();
                }
            });
        }
        latch.await();
        long elapsed = System.currentTimeMillis() - start;
        pool.shutdown();
        
        System.out.printf("[平台线程池 size=%d] 耗时: %d ms, 吞吐: %.0f task/s%n",
                poolSize, elapsed, TASK_COUNT * 1000.0 / elapsed);
    }

    private static void runWithVirtualThreads() throws Exception {
        long start = System.currentTimeMillis();
        CountDownLatch latch = new CountDownLatch(TASK_COUNT);
        
        // 每个任务一个虚拟线程,无需池化
        for (int i = 0; i < TASK_COUNT; i++) {
            Thread.ofVirtual().start(() -> {
                try {
                    Thread.sleep(BLOCK_MILLIS);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    latch.countDown();
                }
            });
        }
        latch.await();
        long elapsed = System.currentTimeMillis() - start;
        
        System.out.printf("[虚拟线程 无限制] 耗时: %d ms, 吞吐: %.0f task/s%n",
                elapsed, TASK_COUNT * 1000.0 / elapsed);
    }

    private static void runWithVirtualThreadsAndSemaphore(int permits) throws Exception {
        long start = System.currentTimeMillis();
        CountDownLatch latch = new CountDownLatch(TASK_COUNT);
        // 信号量限制并发,保护下游服务
        Semaphore semaphore = new Semaphore(permits);
        
        for (int i = 0; i < TASK_COUNT; i++) {
            Thread.ofVirtual().start(() -> {
                try {
                    semaphore.acquire();  // 超过并发上限时挂起虚拟线程
                    try {
                        Thread.sleep(BLOCK_MILLIS);
                    } finally {
                        semaphore.release();
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    latch.countDown();
                }
            });
        }
        latch.await();
        long elapsed = System.currentTimeMillis() - start;
        
        System.out.printf("[虚拟线程+信号量=%d] 耗时: %d ms, 吞吐: %.0f task/s%n",
                permits, elapsed, TASK_COUNT * 1000.0 / elapsed);
    }
}

典型输出(8核机器):

CPU核心数: 8
[平台线程池 size=200] 耗时: 5012 ms, 吞吐: 1995 task/s
[虚拟线程 无限制] 耗时: 145 ms, 吞吐: 68965 task/s
[虚拟线程+信号量=500] 耗时: 2031 ms, 吞吐: 4923 task/s

注意第三个方案的取舍:虚拟线程让吞吐量提升,但如果不加限制,可能瞬间打爆下游。信号量是虚拟线程时代替代线程池做限流的标准做法。

示例2:结构化并发(Structured Concurrency)实战

Java 21引入了StructuredTaskScope(预览特性)作为虚拟线程的黄金搭档。它解决了传统ExecutorService的一个顽疾:任务的生命周期管理混乱,父任务取消时子任务可能仍在跑。

import java.util.concurrent.*;
import jdk.incubator.concurrent.StructuredTaskScope;

/**
 * 场景:聚合用户信息,需要并发调用三个下游服务
 * 任一失败则整体失败,且必须取消其他正在执行的任务
 * 编译需加: --enable-preview --release 21
 */
public class StructuredConcurrencyDemo {

    record UserProfile(String basic, String orders, String recommendations) {}

    public static void main(String[] args) throws Exception {
        // 正常场景
        System.out.println(fetchProfile(1001L, false));
        // 失败场景:orders服务异常
        System.out.println(fetchProfile(1002L, true));
    }

    static UserProfile fetchProfile(long userId, boolean failOrders) throws Exception {
        // ShutdownOnFailure: 任一子任务失败,取消其他所有子任务
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            
            Subtask<String> basic = scope.fork(() -> fetchBasic(userId));
            Subtask<String> orders = scope.fork(() -> fetchOrders(userId, failOrders));
            Subtask<String> recs = scope.fork(() -> fetchRecommendations(userId));
            
            // 等待所有子任务完成,或任一失败
            scope.join();
            // 如果有失败,抛出异常(会包含所有失败信息)
            scope.throwIfFailed();
            
            // 到这里说明全部成功,get()不会阻塞
            return new UserProfile(basic.get(), orders.get(), recs.get());
        }
        // try-with-resources 自动关闭scope,确保所有子任务已结束
    }

    static String fetchBasic(long userId) throws InterruptedException {
        Thread.sleep(50);
        return "basic-" + userId;
    }

    static String fetchOrders(long userId, boolean fail) throws InterruptedException {
        Thread.sleep(100);
        if (fail) throw new RuntimeException("订单服务不可用");
        return "orders-" + userId;
    }

    static String fetchRecommendations(long userId) throws InterruptedException {
        Thread.sleep(80);
        return "recs-" + userId;
    }
}

结构化并发的价值在于:代码的块结构(block structure)与任务的生命周期结构一一对应。try块结束,所有子任务保证已终止。这消除了传统并发代码中"泄漏的线程"和"孤儿任务"的问题。

示例3:Spring Boot 3.2 + 虚拟线程集成

Spring Boot 3.2(Spring Framework 6.1)正式支持虚拟线程。只需一行配置:

# application.yml
spring:
  threads:
    virtual:
      enabled: true

但生产环境需要更精细的控制。下面是一个自定义配置:

import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.support.TaskExecutorAdapter;

import java.util.concurrent.Executors;

@Configuration
public class VirtualThreadConfig {

    /**
     * Tomcat 使用虚拟线程处理请求
     * 注意:Tomcat 仍需少量平台线程做 accept/poll,但请求处理走虚拟线程
     */
    @Bean
    public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
        return protocolHandler -> {
            protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
        };
    }

    /**
     * @Async 注解使用的执行器也切换到虚拟线程
     */
    @Bean(name = "applicationTaskExecutor")
    public AsyncTaskExecutor applicationTaskExecutor() {
        return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
    }
}

配套的Service层代码:

import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

import java.util.concurrent.*;

@Service
public class OrderQueryService {

    private final RestClient restClient;
    // 关键:用信号量限制对下游的并发,替代线程池的限流作用
    private final Semaphore downstreamLimit = new Semaphore(200);

    public OrderQueryService(RestClient.Builder builder) {
        this.restClient = builder.baseUrl("https://api.downstream.com").build();
    }

    public OrderDetail query(Long orderId) throws Exception {
        // 虚拟线程中,Semaphore.acquire() 会挂起而非阻塞载体线程
        downstreamLimit.acquire();
        try {
            // 同步阻塞式代码,但底层走虚拟线程的挂起机制
            Order order = restClient.get()
                    .uri("/orders/{id}", orderId)
                    .retrieve()
                    .body(Order.class);
            
            // 并发调用两个独立下游,用结构化并发聚合
            try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
                Subtask<User> user = scope.fork(() -> 
                        restClient.get().uri("/users/{id}", order.getUserId())
                                .retrieve().body(User.class));
                Subtask<Logistics> logistics = scope.fork(() -> 
                        restClient.get().uri("/logistics/{id}", orderId)
                                .retrieve().body(Logistics.class));
                
                scope.join();
                scope.throwIfFailed();
                
                return new OrderDetail(order, user.get(), logistics.get());
            }
        } finally {
            downstreamLimit.release();
        }
    }

    record Order(Long id, Long userId) {}
    record User(Long id, String name) {}
    record Logistics(String status) {}
    record OrderDetail(Order order, User user, Logistics logistics) {}
}

方案对比:虚拟线程 vs 响应式 vs 传统线程池

维度 传统线程池 响应式(Reactor/WebFlux) 虚拟线程
编程模型 同步阻塞 异步回调/声明式 同步阻塞
代码可读性 高 低(链式操作符) 高
调试体验 好(栈完整) 差(栈割裂) 好(栈完整)
吞吐量(I/O密集) 受线程数限制 极高 极高
吞吐量(CPU密集) 好 好 好(无额外收益)
内存占用/并发单元 ~1MB ~几百字节 ~几百字节起
背压支持 需手动 原生支持 需手动(信号量)
生态成熟度 极高 高 快速增长中
学习曲线 低 高 低
适用场景 传统应用 高并发网关、流处理 高并发I/O密集型应用

核心结论:

  1. 虚拟线程不是银弹。CPU密集型任务用虚拟线程没有收益,甚至因为调度开销略慢于平台线程。
  2. 虚拟线程不是响应式的替代品。响应式在流处理、背压、事件驱动场景仍有独特优势。虚拟线程的价值是"让普通开发者用同步代码获得异步性能"。
  3. 两者可以共存。R2DBC等响应式驱动可以与虚拟线程配合,但要注意不要在虚拟线程内做响应式的阻塞等待,会造成载体线程固定。

最佳实践与避坑指南

坑1:synchronized 导致载体线程固定(Pinning)

这是虚拟线程最经典的陷阱。

// ❌ 危险:synchronized 块内阻塞,会固定载体线程
public synchronized String badQuery(Long id) throws InterruptedException {
    Thread.sleep(1000);  // 虚拟线程被钉在载体线程上,无法卸载
    return "result";
}

// ✅ 正确:用 ReentrantLock 替代
private final ReentrantLock lock = new ReentrantLock();

public String goodQuery(Long id) throws InterruptedException {
    lock.lock();
    try {
        Thread.sleep(1000);  // 可以正常卸载
        return "result";
    } finally {
        lock.unlock();
    }
}

检测方法:启动时加-Djdk.tracePinnedThreads=full,JVM会打印所有固定事件及栈信息。JDK 24(JEP 491)已解决synchronized的固定问题,但在JDK 21上仍需警惕。

坑2:ThreadLocal 的内存放大

虚拟线程数量可能是平台线程的千倍,如果每个虚拟线程都持有ThreadLocal,内存会爆炸。

// ❌ 危险:每个虚拟线程都缓存一个大对象
private static final ThreadLocal<byte[]> BUFFER = 
        ThreadLocal.withInitial(() -> new byte[1024 * 1024]);  // 1MB

// ✅ 推荐:使用 ScopedValue(JDK 21预览)
private static final ScopedValue<UserContext> USER_CTX = ScopedValue.newInstance();

public void handle(Request req) {
    ScopedValue.where(USER_CTX, loadUser(req)).run(() -> {
        // 在作用域内,USER_CTX.get() 可用
        processOrder();
    });
    // 作用域结束,自动清理,无内存泄漏
}

坑3:线程池思维惯性

虚拟线程不应该池化。池化平台线程是因为创建成本高;虚拟线程创建成本极低(纳秒级),池化反而增加复杂度且丧失弹性。

// ❌ 错误:给虚拟线程建池
ExecutorService pool = Executors.newFixedThreadPool(1000, 
        Thread.ofVirtual().factory());

// ✅ 正确:每任务一个虚拟线程
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

坑4:文件I/O不会卸载

JDK 21中,FileInputStream、FileOutputStream的读写是阻塞的,且不会卸载虚拟线程。原因在于文件I/O在多数OS上没有异步接口。

// ❌ 虚拟线程中做文件I/O会固定载体线程
try (var fis = new FileInputStream("large.txt")) {
    fis.read(buffer);  // 固定
}

// ✅ 用异步文件通道
try (var channel = AsynchronousFileChannel.open(Path.of("large.txt"))) {
    // 通过 Future 或 CompletionHandler 异步读取
}

最佳实践清单

  1. 限流用信号量,不用线程池:Semaphore是虚拟线程时代的标准限流工具。
  2. 优先使用结构化并发:StructuredTaskScope让并发代码的异常传播和取消语义清晰可控。
  3. 监控载体线程池:通过jdk.virtualThreadScheduler.parallelism和jdk.virtualThreadScheduler.maxPoolSize调优,默认等于CPU核数。
  4. 迁移前做固定检测:用-Djdk.tracePinnedThreads扫描存量代码。
  5. 不要在虚拟线程里做CPU密集计算:这不解决问题,反而增加调度开销。
  6. 注意第三方库的兼容性:老的连接池(如HikariCP早期版本)、Netty的某些用法可能与虚拟线程不兼容。

总结

回顾全文的核心要点:

  1. 本质:虚拟线程是JVM调度的轻量级执行单元,通过Continuation的栈帧堆化实现挂起/恢复,通过ForkJoinPool调度到少量载体线程上运行。
  1. 价值:让开发者用同步阻塞的编程模型,获得接近异步编程的吞吐量。它不改变Java的并发语义,只是让"一请求一线程"重新变得可行。
  1. 代价:synchronized固定、ThreadLocal内存放大、文件I/O不卸载——这些是JDK 21上的现实约束,部分已在后续版本修复。
  1. 定位:虚拟线程不是响应式的替代,而是补充。它降低了高并发编程的门槛,让普通业务开发者也能写出高吞吐的服务。

延伸思考:

  • JEP 491(JDK 24)解决了synchronized固定问题,意味着存量代码迁移的最大障碍被移除。
  • ScopedValue将在未来替代ThreadLocal,成为虚拟线程时代的上下文传递标准。
  • 结构化并发仍在预览,一旦转正,将与虚拟线程共同构成Java并发的"新双核"。
  • 当线程变得廉价,"线程池"这个模式本身可能逐步退出历史舞台,取而代之的是"信号量+虚拟线程"的组合。

Java的并发模型正在经历一次范式转移。掌握虚拟线程,不仅是学习一个新API,更是理解JVM如何重新思考"什么是线程"这个根本问题。