什么是虚拟线程?
在 Java 21 之前,Java 的Thread是对操作系统线程(也称为平台线程)的一层薄封装,两者是 1:1 的关系。这意味着:
- 资源开销大:每个平台线程需要占用约 1MB 的栈内存,创建数千个线程就可能耗尽系统内存。
- 调度成本高:线程的创建、销毁和上下文切换都由操作系统内核完成,成本高昂。
虚拟线程则完全不同:
- 轻量级:它是由 JVM 管理和调度的用户态线程,不直接对应操作系统线程。
- M:N 模型:大量的虚拟线程(M)会被复用到少量的平台线程(N,也称为载体线程/Carrier Thread)上执行。
- 成本极低:创建一个虚拟线程仅消耗几百字节的内存,可以轻松创建百万级别的虚拟线程。
虚拟线程 vs. 平台线程
表格
| 特性 | 平台线程 (Platform Thread) | 虚拟线程 (Virtual Thread) |
|---|---|---|
| 调度者 | 操作系统 (OS) | Java 虚拟机 (JVM) |
| 内存开销 | 高 (默认 ~1MB 栈空间) | 极低 (初始几百字节,动态伸缩) |
| 最大数量 | 有限 (通常几千个) | 极高 (百万级) |
| 适用场景 | CPU 密集型任务 | I/O 密集型任务 |
| 阻塞行为 | 阻塞整个 OS 线程,资源浪费 | 自动挂起,释放载体线程给其他任务 |
如何使用虚拟线程
使用虚拟线程非常简单,主要有以下几种方式:
1. 使用ThreadAPI 直接创建
// 方式一:启动一个虚拟线程 Thread vThread = Thread.startVirtualThread(() -> { System.out.println("Hello from virtual thread: " + Thread.currentThread()); }); // 方式二:使用 Builder 模式 Thread vThread2 = Thread.ofVirtual() .name("my-virtual-thread") .unstarted(() -> { // 你的任务逻辑 }); vThread2.start();2. 使用 ExecutorService (推荐)
这是与现有线程池代码集成最方便的方式。
// 为每个任务创建一个虚拟线程的 ExecutorService try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 10_000; i++) { executor.submit(() -> { // 模拟耗时 I/O 操作 Thread.sleep(1000); return "任务完成"; }); } } // try-with-resources 会自动关闭 executor避坑指南与最佳实践
虽然虚拟线程非常强大,但在使用时需要注意以下几点,否则可能无法发挥其优势,甚至导致性能下降。
1. 警惕synchronized导致的“钉住”现象
这是最重要的一点!当一个虚拟线程在执行synchronized代码块或方法时发生阻塞(如 I/O 操作),它会连带着承载它的载体线程(Carrier Thread)一起被阻塞。这种现象被称为“钉住”(Pinning)。
这会严重损害性能,因为宝贵的载体线程被浪费了。
解决方案:
将synchronized替换为java.util.concurrent.locks.ReentrantLock。ReentrantLock是在 Java 层面实现的,能与虚拟线程的挂起机制完美配合,不会导致钉住。
// 不推荐:可能导致载体线程被钉住 private final Object lock = new Object(); public void doWork() { synchronized (lock) { // 如果有 I/O 阻塞,会 pin 住载体线程 someIoOperation(); } } // 推荐:使用 ReentrantLock private final ReentrantLock lock = new ReentrantLock(); public void doWork() throws InterruptedException { lock.lock(); try { // 即使有 I/O 阻塞,虚拟线程也能正确挂起 someIoOperation(); } finally { lock.unlock(); } }注意:如果synchronized块内只进行纯 CPU 计算,不涉及任何阻塞调用,那么它是安全的。
2. 谨慎使用ThreadLocal
ThreadLocal在每个线程中存储一份变量副本。在虚拟线程时代,你可以轻松创建百万个线程,如果大量使用ThreadLocal,可能会导致巨大的内存开销。
未来方案:
Java 正在引入ScopedValue作为ThreadLocal的替代品,它专为虚拟线程设计,内存效率更高。
3. 确保第三方库兼容
一些老旧的第三方库(尤其是数据库连接池)可能在内部使用了synchronized包裹阻塞操作。这同样会导致钉住问题。务必将常用库(如 HikariCP、Hibernate 等)升级到支持虚拟线程的最新版本。
4. 监控“钉住”情况
在生产环境中,可以通过添加 JVM 参数来监控是否有线程被钉住,以便及时发现和解决问题。
java -Djdk.tracePinnedThreads=short -jar your-app.jar