Java 虚拟线程
Java 虚拟线程(Virtual Threads)是 Java 19 引入的一项特性,并在 Java 21 成为正式特性。虚拟线程是轻量级的线程实现,旨在提高 Java 程序在高并发场景下的性能和可扩展性,特别适合处理大量并发任务(如 I/O 密集型任务)。它通过将用户线程与平台线程(OS 线程)解耦,极大降低了线程创建和管理的开销。
一、虚拟线程的核心概念
什么是虚拟线程?
- 虚拟线程是 JVM 管理的轻量级线程,不直接绑定到操作系统线程(称为平台线程)。
- 虚拟线程由 JVM 的调度器管理,运行在少量的平台线程(称为载体线程,carrier threads)上,JVM 通过协程机制在平台线程上多路复用大量虚拟线程。
- 一个 Java 应用程序可以创建数百万个虚拟线程,而不会导致显著的性能开销。
虚拟线程 vs 平台线程
特性 虚拟线程 平台线程 资源占用 轻量级,内存占用低(KB 级别, 一般2KB) 重量级,内存占用高(MB 级别) 创建成本 低,几乎无限制(可创建百万级) 高,受限于 OS 资源 调度方式 JVM 调度,基于协程 OS 调度 适用场景 高并发、I/O 密集型任务 CPU 密集型任务 虚拟线程的核心优势
- 高并发性:支持创建大量线程,适合高并发场景(如 Web 服务器处理大量请求)。
- 简化并发模型:虚拟线程允许使用传统的阻塞式编程模型(如
Thread.sleep
、synchronized
),而无需复杂的异步编程(如CompletableFuture
或回调)。 - 兼容性:虚拟线程与现有 Java 代码和库高度兼容,无需大幅修改代码。
- 低开销:虚拟线程的创建、销毁和上下文切换成本极低。
工作原理
- 虚拟线程基于 Continuation(延续)机制实现,JVM 可以在虚拟线程阻塞时将其挂起(unmount),释放底层平台线程供其他虚拟线程使用。
- 当虚拟线程的阻塞操作完成(如 I/O 操作),JVM 将其重新挂载(mount)到某个平台线程继续执行。
- 虚拟线程的调度由 JVM 的
ForkJoinPool
(默认)管理,平台线程池大小通常与 CPU 核心数相关。
二、虚拟线程的用法
虚拟线程可以通过以下几种方式创建和使用:
1. 使用 Thread.ofVirtual()
创建虚拟线程
Thread.ofVirtual()
是创建虚拟线程的工厂方法,返回一个Thread.Builder
。- 示例代码:
public class VirtualThreadExample { public static void main(String[] args) throws InterruptedException { // 创建并启动虚拟线程 Thread virtualThread = Thread.ofVirtual().start(() -> { System.out.println("Running in virtual thread: " + Thread.currentThread()); try { Thread.sleep(1000); // 模拟阻塞操作 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Task completed in virtual thread"); }); virtualThread.join(); // 等待虚拟线程完成 } }
- 输出示例:
Running in virtual thread: VirtualThread[#1]/runnable@ForkJoinPool-1-worker-1 Task completed in virtual thread
- 说明:
Thread.ofVirtual().start()
直接启动一个虚拟线程,Thread.currentThread()
显示这是一个虚拟线程,运行在ForkJoinPool
的某个工作线程上。
2. 使用 Executors.newVirtualThreadPerTaskExecutor()
Executors
提供了一个便捷的执行器,专门为虚拟线程设计,每个任务都会创建一个新的虚拟线程。- 示例代码:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class VirtualThreadExecutorExample { public static void main(String[] args) { try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 5; i++) { final int taskId = i; executor.submit(() -> { System.out.println("Task " + taskId + " running in: " + Thread.currentThread()); try { Thread.sleep(1000); // 模拟 I/O 操作 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Task " + taskId + " completed"); }); } } // try-with-resources 自动关闭 executor } }
- 输出示例(可能因调度顺序不同):
Task 0 running in: VirtualThread[#1]/runnable@ForkJoinPool-1-worker-1 Task 1 running in: VirtualThread[#2]/runnable@ForkJoinPool-1-worker-2 Task 2 running in: VirtualThread[#3]/runnable@ForkJoinPool-1-worker-3 Task 3 running in: VirtualThread[#4]/runnable@ForkJoinPool-1-worker-4 Task 4 running in: VirtualThread[#5]/runnable@ForkJoinPool-1-worker-5 Task 0 completed Task 1 completed Task 2 completed Task 3 completed Task 4 completed
- 说明:
newVirtualThreadPerTaskExecutor
为每个提交的任务创建一个虚拟线程,适合高并发任务的分发。
3. 使用 ThreadFactory
创建虚拟线程
- 如果需要自定义线程属性,可以使用
Thread.ofVirtual().factory()
创建线程工厂。 - 示例代码:
import java.util.concurrent.ThreadFactory; public class VirtualThreadFactoryExample { public static void main(String[] args) throws InterruptedException { ThreadFactory factory = Thread.ofVirtual().name("my-virtual-thread-", 0).factory(); Thread virtualThread = factory.newThread(() -> { System.out.println("Running in: " + Thread.currentThread()); }); virtualThread.start(); virtualThread.join(); } }
- 输出示例:
Running in: VirtualThread[#my-virtual-thread-0]/runnable@ForkJoinPool-1-worker-1
- 说明:
name("prefix", startIndex)
可以为虚拟线程设置自定义名称,方便调试。
4. 虚拟线程与结构化并发(Structured Concurrency)
- Java 提供
StructuredTaskScope
(JEP 428)来管理一组虚拟线程的任务,确保任务以结构化的方式完成。 - 示例代码(需要 Java 21+,预览特性):
import jdk.incubator.concurrent.StructuredTaskScope; public class StructuredConcurrencyExample { public static void main(String[] args) throws Exception { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { var task1 = scope.fork(() -> { Thread.sleep(1000); return "Result from task 1"; }); var task2 = scope.fork(() -> { Thread.sleep(500); return "Result from task 2"; }); scope.join(); // 等待所有任务完成 scope.throwIfFailed(); // 如果有任务失败,抛出异常 System.out.println(task1.get() + ", " + task2.get()); } } }
- 输出示例:
Result from task 1, Result from task 2
- 说明:
StructuredTaskScope
确保所有子任务完成或失败时进行统一管理,简化并发任务的协调。
三、虚拟线程的使用场景
高并发 I/O 密集型应用
- 例如 Web 服务器(如 Spring Boot、Tomcat、Jetty)处理大量 HTTP 请求,每个请求可以分配一个虚拟线程,阻塞式代码更简单且性能接近异步模型。
- 示例:处理 10,000 个并发 HTTP 请求,每个请求调用外部 API,虚拟线程可以轻松应对。
任务分解
- 结构化并发适合将复杂任务分解为多个子任务(如并行处理多个数据库查询或 API 调用)。
替换异步编程
- 虚拟线程允许用简单的同步代码替换复杂的
CompletableFuture
或回调地狱,降低代码复杂度。
- 虚拟线程允许用简单的同步代码替换复杂的
微服务和云原生
- 在微服务架构中,虚拟线程可以提高服务吞吐量,减少对线程池配置的依赖。
四、虚拟线程的注意事项
阻塞操作的性能
- 虚拟线程适合 I/O 密集型任务(如网络请求、文件读写)。对于 CPU 密集型任务(如复杂计算),虚拟线程的性能可能不如平台线程,因为它们共享有限的载体线程。
线程本地变量(ThreadLocal)
- 虚拟线程不适合大量使用
ThreadLocal
,因为虚拟线程数量可能非常多,且生命周期较短,可能导致内存浪费或不必要的数据拷贝。 - 建议使用
ThreadLocal
时评估是否可以用其他方式(如传递参数)替代,或者 ScopedValue。
- 虚拟线程不适合大量使用
同步块(synchronized)
- 使用
synchronized
时要小心,因为它可能导致虚拟线程绑定到平台线程(pinning),降低并发性能。建议使用ReentrantLock
替代。 - 示例:避免 pinning 的代码
import java.util.concurrent.locks.ReentrantLock; public class AvoidPinning { private static final ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 5; i++) { executor.submit(() -> { lock.lock(); try { System.out.println("Task running in: " + Thread.currentThread()); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }); } } } }
- 使用
调试和监控
- 虚拟线程在调试时可能难以跟踪,因为它们是轻量级的,且线程名称可能重复。可以通过
Thread.ofVirtual().name()
设置有意义的名称。 - 使用工具(如 JFR 或 VisualVM)监控虚拟线程的运行状态。
- 虚拟线程在调试时可能难以跟踪,因为它们是轻量级的,且线程名称可能重复。可以通过
兼容性问题
- 虚拟线程与某些 JNI 代码或旧版库可能不完全兼容(如绑定到特定平台线程的库)。
- 如果使用第三方库,确保其支持虚拟线程或不依赖平台线程。
资源限制
- 尽管虚拟线程轻量,但创建过多虚拟线程仍可能耗尽 JVM 内存或 CPU 资源。建议根据实际需求测试和调整。
五、虚拟线程的局限性
不适合 CPU 密集型任务
- 虚拟线程的调度依赖于有限的平台线程,CPU 密集型任务可能导致载体线程饱和,降低性能。
Pinning 问题
- 在
synchronized
块或 JNI 调用中,虚拟线程可能被“钉”到平台线程,失去轻量级的优势。
- 在
预览特性(早期版本)
- 在 Java 19 和 20 中,虚拟线程是预览特性(需要
--enable-preview
),在 Java 21 才正式稳定。
- 在 Java 19 和 20 中,虚拟线程是预览特性(需要
六、实际应用示例:Web 服务器
以下是一个使用虚拟线程的简单 HTTP 服务器示例,展示如何处理多个并发请求:
import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
public class VirtualThreadHttpServer {
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
server.createContext("/", exchange -> {
String response = "Hello from virtual thread: " + Thread.currentThread();
Thread.sleep(1000); // 模拟处理时间
exchange.sendResponseHeaders(200, response.length());
exchange.getResponseBody().write(response.getBytes());
exchange.close();
});
server.start();
System.out.println("Server started on port 8080");
}
}
- 说明:每个 HTTP 请求都在一个虚拟线程中处理,即使有数千个并发请求,服务器也能高效运行。
七、总结
Java 虚拟线程通过轻量级的线程模型,极大简化了高并发编程,特别适合 I/O 密集型任务。开发者可以继续使用熟悉的阻塞式编程风格,而无需复杂异步代码。以下是关键点:
- 创建方式:
Thread.ofVirtual()
、Executors.newVirtualThreadPerTaskExecutor()
或StructuredTaskScope
。 - 适用场景:高并发 I/O 操作、Web 服务器、微服务等。
- 注意事项:避免 pinning、谨慎使用
ThreadLocal
、监控资源使用。 - 未来发展:虚拟线程是 Java 并发模型的重要进步,与 Project Loom 的其他特性(如结构化并发)结合,将进一步提升 Java 在高并发场景下的竞争力。