在 Java 25 中,虚拟线程已作为一项成熟稳定的特性被正式引入,其核心目标在于简化高并发应用程序的编写,尤其擅长处理大量 I/O 密集型任务。相较于传统平台线程,虚拟线程的创建和上下文切换成本极低,允许开发者以"一个请求一个线程"的同步编程模型来构建高吞吐量的服务。
基础创建与使用
虚拟线程的创建主要有三种方式:Thread.ofVirtual()、Thread.startVirtualThread() 以及通过 Executors.newVirtualThreadPerTaskExecutor()。
方式一:使用 Thread.startVirtualThread(Runnable task)
这是启动一个虚拟线程最简单直接的方法。
java
public class BasicVirtualThreadDemo {
public static void main(String[] args) throws InterruptedException {
// 启动一个虚拟线程执行任务
Thread vThread = Thread.startVirtualThread(() -> {
System.out.println("Hello from a virtual thread!");
System.out.println("Thread name: " + Thread.currentThread().getName());
System.out.println("Is virtual: " + Thread.currentThread().isVirtual());
});
// 等待虚拟线程执行完毕
vThread.join();
}
}
运行上述代码,输出会显示线程名称以 VirtualThread 开头,并且 isVirtual() 方法返回 true 。
方式二:使用 Thread.ofVirtual().start(Runnable task)
Thread.ofVirtual() 返回一个 Thread.Builder,提供了更丰富的线程配置选项。
java
public class BuilderVirtualThreadDemo {
public static void main(String[] args) {
// 使用Builder创建并启动虚拟线程,可以设置线程名称
Thread.Builder builder = Thread.ofVirtual().name("my-vthread-", 0);
Thread vThread = builder.start(() -> {
System.out.println("Task executed by: " + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模拟I/O阻塞操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task completed.");
});
try {
vThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
方式三:使用 Executors.newVirtualThreadPerTaskExecutor()
对于需要管理多个并发任务的场景,使用 ExecutorService 是更佳实践。Java 25 提供了专门的执行器服务来管理虚拟线程。
java
import java.util.concurrent.*;
public class ExecutorVirtualThreadDemo {
public static void main(String[] args) {
// 创建使用虚拟线程的执行器
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 提交多个任务
Future<String> future1 = executor.submit(() -> {
Thread.sleep(500);
return "Result from task 1";
});
Future<Integer> future2 = executor.submit(() -> {
Thread.sleep(300);
return 42;
});
// 获取任务结果
System.out.println(future1.get()); // 输出: Result from task 1
System.out.println(future2.get()); // 输出: 42
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
// try-with-resources 会自动关闭ExecutorService
}
}
这种执行器会为每个提交的任务创建一个新的虚拟线程,非常适合处理大量短生命周期的异步任务 。
与结构化并发结合使用
Java 25 中,虚拟线程与结构化并发(StructuredTaskScope)的结合是处理复杂并发流程的推荐模式。结构化并发强制要求任务在明确的作用域内创建和完成,简化了错误处理和资源管理 。
java
import java.util.concurrent.*;
import java.util.concurrent.StructuredTaskScope.*;
public class StructuredConcurrencyDemo {
public static void main(String[] args) throws Exception {
// 使用结构化并发作用域处理并发任务
String result = handleUserRequest();
System.out.println("Final result: " + result);
}
static String handleUserRequest() throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// 在作用域内派生子任务(每个子任务运行在独立的虚拟线程上)
Subtask<String> userSubtask = scope.fork(() -> fetchUserInfo("user123"));
Subtask<Integer> orderSubtask = scope.fork(() -> fetchOrderCount("user123"));
// 等待所有子任务完成或任一失败
scope.join();
scope.throwIfFailed(); // 如果任何子任务失败,抛出异常
// 组合结果
String userInfo = userSubtask.get();
int orderCount = orderSubtask.get();
return String.format("User: %s, Orders: %d", userInfo, orderCount);
}
}
static String fetchUserInfo(String userId) throws InterruptedException {
Thread.sleep(200); // 模拟网络I/O
return "Alice";
}
static Integer fetchOrderCount(String userId) throws InterruptedException {
Thread.sleep(150); // 模拟数据库I/O
return 5;
}
}
在这个例子中,fetchUserInfo 和 fetchOrderCount 两个 I/O 操作并发执行,每个都在自己的虚拟线程中运行。StructuredTaskScope 确保了两个任务要么都成功完成,要么在发生错误时一起被取消,避免了资源泄漏 。
处理阻塞操作与性能对比
虚拟线程的关键优势在于处理阻塞操作(如 I/O、锁等待)时的高效性。当虚拟线程发生阻塞时,JVM 会将其挂起,并释放其载体线程(一个真正的平台线程)去执行其他就绪的虚拟线程,从而用极少的硬件线程支撑海量并发。
java
import java.util.concurrent.*;
public class IOBoundTaskDemo {
public static void main(String[] args) {
int taskCount = 10_000;
System.out.println("模拟 " + taskCount + " 个I/O密集型任务...");
long startTime = System.currentTimeMillis();
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 提交大量模拟I/O阻塞的任务
for (int i = 0; i < taskCount; i++) {
final int taskId = i;
executor.submit(() -> {
try {
// 模拟一个耗时的I/O操作,如数据库查询或HTTP调用
Thread.sleep(10);
// System.out.println("Task " + taskId + " completed on " + Thread.currentThread().getName());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
} // ExecutorService 自动关闭,会等待所有任务完成
long duration = System.currentTimeMillis() - startTime;
System.out.println("所有任务完成,总耗时: " + duration + " ms");
}
}
如果使用传统的固定大小线程池(例如 Executors.newFixedThreadPool(200))来处理上万个阻塞任务,由于线程数量有限,大部分任务将排队等待,总耗时会显著增加。而虚拟线程方案可以几乎同时启动所有任务,总耗时主要取决于单个任务的阻塞时间(约10ms),而非任务数量,从而极大提升了吞吐量 。
重要注意事项与最佳实践
- 线程局部变量 :虚拟线程支持
ThreadLocal,但由于可以创建海量虚拟线程,需谨慎使用以避免内存消耗过大。对于大量可复用的上下文信息,考虑使用ScopedValue(Java 20+ 引入的预览特性,在 Java 25 中可能已稳定或继续演进)。 - 线程池 :不要将虚拟线程池化。虚拟线程的设计初衷就是廉价、可随意创建和销毁。应始终使用
Executors.newVirtualThreadPerTaskExecutor()或类似机制,而不是尝试复用虚拟线程对象 。 - 同步操作 :在虚拟线程内部执行
synchronized块或ReentrantLock.lock()等同步操作时,会阻塞其载体线程。如果同步操作频繁或耗时,可能会抵消虚拟线程的优势。在关键路径上,优先考虑使用java.util.concurrent包中的并发工具(如ReentrantLock配合tryLock)或异步 API 。 - 调试与监控 :Java 25 增强了虚拟线程的调试和监控能力。可以使用
jcmd <pid> Thread.dump_to_file -format=json <file>生成包含虚拟线程的线程转储,或通过 JMX 的java.lang.management.ThreadMXBean来监控虚拟线程的状态 。
启用虚拟线程
在 Java 25 中,虚拟线程是默认启用的核心特性,无需特殊 JVM 参数即可使用 。对于从早期预览版升级的项目,只需确保移除了相关的 --enable-preview 标志即可。