深入分析---虚拟线程VS传统多线程
目录
一、环境要求
二、为什么引进虚拟线程
三、创建虚拟线程的常用方法
1. 使用Thread.startVirtualThread()
结果
编辑
2. 使用Thread.Builder
结果
3. 使用ExecutorService
4.使用ThreadFactory
解释(参见注释)
四、虚拟线程&传统线程(代码比较)
1.创建10000个线程的对比
①传统多线程
解释
结果
②虚拟线程
结果
2.Web服务器请求处理对比
我们将关注下面四项指标
服务器端实现
①平台线程池服务器(8080)
②虚拟线程服务器(8085)
测试客户端实现
结果
五、虚拟线程的实现原理
1. 调度模型
2. 载体线程(Carrier Threads)
3. 栈管理
4. 挂起与恢复机制
六、虚拟线程的适用场景
1. 理想使用场景
2. 不适用场景
七、对比
什么叫"自动卸载"?
比喻
在Java并发编程领域,虚拟线程(Virtual Threads)作为Java 19引入的预览特性并在Java 21中正式发布,代表了并发模型的一次重大革新。本文将深入探讨虚拟线程与传统平台线程(Platform Threads)的区别,通过代码示例展示它们的实现方式,分析性能差异,并详细解析虚拟线程的创建方法和底层原理。
一、环境要求
Idea版本:2024.x+
Jdk版本:21+
二、为什么引进虚拟线程
一句话总结:把“线程”变成和对象一样轻,可以随手开几十万甚至上百万条并发任务,而不用担心内存被栈吃光、CPU 被调度拖垮。
虚拟线程引进本来就是为了解决传统多线程的硬伤:
太重:一个平台线程栈默认 1 MB,10 万条≈100 GB 内存,直接 OOM。
太少:操作系统线程是稀缺资源,几万条就把调度器压垮。
太贵:每次创建/销毁、上下文切换都要经过内核,CPU 时间浪费在调度而不是业务。
虚拟线程把“线程”变成 用户态对象+少量栈缓冲,创建/阻塞/切换都在 JVM 里完成,从而把“并发数量级”从 千级 提升到 百万级,同时保持与原有 Thread API 100 % 兼容。
三、创建虚拟线程的常用方法
1. 使用Thread.startVirtualThread()
Thread.startVirtualThread(() -> {System.out.println("Virtual thread running: " + Thread.currentThread());
});
【注】在 Lambda 表达式中,
() -> { ... }
就是Runnable
的简化写法,大括号里的代码就是线程要执行的逻辑。所以上面的代码其实等价于:
Thread.startVirtualThread(new Runnable() {@Overridepublic void run() {System.out.println("Virtual thread running: " + Thread.currentThread());} });
结果
同理,还等价于:
// 定义一个Runnable任务Runnable task = () -> {// 在这里放置你的任务代码System.out.println("Virtual thread1 running...");};// 直接使用Thread类的静态方法startVirtualThread来创建并启动一个虚拟线程Thread vt =Thread.startVirtualThread(task);vt.join();
上面三种表述都是一样的效果。
2. 使用Thread.Builder
Thread.Builder builder = Thread.ofVirtual().name("virtual-thread-", 1);
builder.start(() -> {System.out.println("Named virtual thread running");
});
结果
3. 使用ExecutorService
// 使用线程池创建虚拟线程
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {executor.submit(() -> {System.out.println("通过线程池创建的虚拟线程");});
} // 自动等待所有任务完成
【注】使用
ExecutorService
(通过Executors.newVirtualThreadPerTaskExecutor()
创建)配合try-with-resources
语法,确实会自动等待所有提交的任务完成,无需手动调用join()
方法。try (资源声明1; 可选资源2; …) {// 使用资源的代码块 }
4.使用ThreadFactory
ThreadFactory factory = Thread.ofVirtual().factory();
Thread virtualThread = factory.newThread(() -> {System.out.println("Virtual thread created via factory");
});
virtualThread.start();
解释(参见注释)
package com.qcby.vtTest;import java.util.concurrent.ThreadFactory;public class CreatVT {public static void main(String[] args) throws InterruptedException {/** 让 JVM 返回一个“专门生产虚拟线程”的工厂对象。以后每调用一次 factory.newThread(task),就得到一条 尚未启动 的虚拟线程实例。* */ThreadFactory factory = Thread.ofVirtual().factory();/** 把 Runnable 任务交给工厂,工厂把它包装成一条 新的虚拟线程对象 virtualThread。此时线程处于 NEW 状态,还没开始跑。* */Thread vt = factory.newThread(() -> {System.out.println("Virtual thread created via factory");});/*真正触发 JVM 把这条虚拟线程调度到 极少量 carrier 线程上运行。线程进入 RUNNABLE 状态,随后执行 System.out.println(...)* */vt.start();vt.join();} }
【注】工厂模式只是把“创建线程”和“启动线程”解耦;底层仍是一条虚拟线程。
四、虚拟线程&传统线程(代码比较)
1.创建10000个线程的对比
①传统多线程
// 不推荐实际运行,可能导致系统资源耗尽
List<Thread> platformThreads = new ArrayList<>();
for (int i = 0; i < 10_000; i++) {Thread thread = new Thread(() -> { //循环10000次创建线程try {Thread.sleep(Duration.ofSeconds(1));} catch (InterruptedException e) {e.printStackTrace();}});thread.start();platformThreads.add(thread);
}// 等待所有线程完成
for (Thread thread : platformThreads) {thread.join();
}
解释
向操作系统申请栈内存(默认 1 MB),合计 ≈ 10 GB;
在内核调度器里注册 10 000 个调度实体;
同时睡眠 1 秒,期间仍占着栈和调度资源。
结果
内存瞬间暴涨,很可能触发 OutOfMemoryError。
操作系统线程调度开销激增,系统假死或崩溃。
【注】不要去运行,会导致“卡死”现象。
②虚拟线程
package com.qcby.vtTest;import java.time.Duration;
import java.util.*;public class CreatVT {public static void main(String[] args) throws InterruptedException {List<Thread> virtualThreads = new ArrayList<>();int num=0;for (int i = 0; i < 10_000; i++) {Thread thread = Thread.startVirtualThread(() -> {try {Thread.sleep(Duration.ofSeconds(1));System.out.println(Thread.currentThread().threadId()+"创建成功");} catch (InterruptedException e) {e.printStackTrace();}});virtualThreads.add(thread);}// 等待所有线程完成for (Thread thread : virtualThreads) {thread.join();}}
}
结果
2.Web服务器请求处理对比
Web 服务器请求处理对比,就是把“用传统线程池(Tomcat/Jetty 默认)” 与 “用虚拟线程” 两种方案,放在同一个 Web 服务器场景下,从 并发量、延迟、资源占用 等维度做 A/B 比较。
Web 服务器的场景里,“请求处理” ≈ 并发能力
• 并发量直接决定能同时服务多少用户;
• 并发模型(线程池 vs 虚拟线程)直接决定内存、上下文切换、延迟。
我们将关注下面四项指标
吞吐量(Requests/Second)
延迟(Latency)
资源利用率(CPU、内存)
线程数(活跃线程/虚拟线程挂载数)
服务器端实现
①平台线程池服务器(8080)
import java.io.*;
import java.net.*;
import java.util.concurrent.*;public class ThreadPoolServer {private static final int THREAD_POOL_SIZE = 200;private static final ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);public static void main(String[] args) throws IOException {int port = 8080;try (ServerSocket serverSocket = new ServerSocket(port)) {System.out.println("ThreadPool Server started on port " + port);while (true) {Socket clientSocket = serverSocket.accept();handleRequest(clientSocket);}}}private static void handleRequest(Socket socket) {executor.execute(() -> {try {// 模拟I/O操作InputStream input = socket.getInputStream();byte[] buffer = new byte[1024];int read = input.read(buffer);// 处理请求Thread.sleep(100); // 模拟业务处理时间// 发送响应OutputStream output = socket.getOutputStream();output.write("HTTP/1.1 200 OK\r\n\r\nHello from ThreadPool".getBytes());socket.close();} catch (Exception e) {e.printStackTrace();}});}
}
②虚拟线程服务器(8085)
import java.io.*;
import java.net.*;public class VirtualThreadServer {public static void main(String[] args) throws IOException {int port = 8085;try (ServerSocket serverSocket = new ServerSocket(port)) {System.out.println("VirtualThread Server started on port " + port);while (true) {Socket clientSocket = serverSocket.accept();handleRequest(clientSocket);}}}private static void handleRequest(Socket socket) {Thread.startVirtualThread(() -> {try {// 模拟I/O操作InputStream input = socket.getInputStream();byte[] buffer = new byte[1024];int read = input.read(buffer);// 处理请求Thread.sleep(100); // 模拟业务处理时间// 发送响应OutputStream output = socket.getOutputStream();output.write("HTTP/1.1 200 OK\r\n\r\nHello from VirtualThread".getBytes());socket.close();} catch (Exception e) {e.printStackTrace();}});}
}
测试客户端实现
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;public class LoadTestClient {private static final int TOTAL_REQUESTS = 10_000;private static final int CONCURRENT_CLIENTS = 1_000;public static void main(String[] args) throws InterruptedException {// 测试线程池服务器testServer("localhost", 8080, "ThreadPool Server");// 测试虚拟线程服务器testServer("localhost", 8085, "VirtualThread Server");}private static void testServer(String host, int port, String serverName) throws InterruptedException {final AtomicInteger completedRequests = new AtomicInteger();final AtomicInteger failedRequests = new AtomicInteger();final CountDownLatch latch = new CountDownLatch(CONCURRENT_CLIENTS);long startTime = System.currentTimeMillis();ExecutorService clientExecutor = Executors.newFixedThreadPool(CONCURRENT_CLIENTS);for (int i = 0; i < CONCURRENT_CLIENTS; i++) {clientExecutor.execute(() -> {for (int j = 0; j < TOTAL_REQUESTS / CONCURRENT_CLIENTS; j++) {try (Socket socket = new Socket(host, port);OutputStream out = socket.getOutputStream();InputStream in = socket.getInputStream()) {// 发送简单HTTP请求String request = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";out.write(request.getBytes());out.flush();// 读取响应byte[] response = new byte[1024];int bytesRead = in.read(response);completedRequests.incrementAndGet();} catch (Exception e) {failedRequests.incrementAndGet();}}latch.countDown();});}latch.await();clientExecutor.shutdown();long endTime = System.currentTimeMillis();long totalTime = endTime - startTime;System.out.println("\n" + serverName + " Test Results:");System.out.println("Total requests: " + TOTAL_REQUESTS);System.out.println("Completed requests: " + completedRequests.get());System.out.println("Failed requests: " + failedRequests.get());System.out.println("Total time: " + totalTime + " ms");System.out.println("Throughput: " + (completedRequests.get() / (totalTime / 1000.0)) + " requests/sec");}
}
结果
五、虚拟线程的实现原理
1. 调度模型
虚拟线程采用M:N调度模型:
-
M个虚拟线程映射到N个平台线程(N通常等于CPU核心数)
-
JVM负责虚拟线程的调度,而非操作系统
-
当虚拟线程执行阻塞操作时,JVM会自动将其挂起,释放平台线程
【注】平台线程 = OS 线程 = 传统意义上的 Java 线程(
Thread
或线程池里的工作线程)
平台 = 操作系统(Windows、Linux、macOS)。
平台线程 = 由操作系统直接创建、调度的线程。
2. 载体线程(Carrier Threads)
虚拟线程运行在平台线程(称为载体线程)上:
-
挂起的虚拟线程不占用载体线程
-
一个载体线程可以依次执行多个虚拟线程
-
JVM维护一个ForkJoinPool作为虚拟线程的默认调度器
3. 栈管理
虚拟线程使用"栈切片"技术:
-
栈空间按需分配和释放
-
挂起时保存栈状态,恢复时重建
-
避免了固定大栈的内存浪费
4. 挂起与恢复机制
当虚拟线程遇到阻塞操作时:
-
当前状态(包括栈帧和局部变量)被保存到堆内存
-
载体线程被释放以执行其他虚拟线程
-
当I/O操作完成时,虚拟线程被重新调度到可用载体线程
-
之前的状态被恢复,执行继续
六、虚拟线程的适用场景
1. 理想使用场景
-
高并发I/O密集型应用(如Web服务器)
-
需要处理大量并发连接
-
包含许多阻塞操作的任务
-
需要简单同步代码但又要高性能的场景
2. 不适用场景
-
CPU密集型任务(无性能优势)
-
需要线程本地存储的重度使用
-
依赖线程优先级或精细线程控制的场景
七、对比
平台线程 = 传统 Java 线程 = 操作系统线程;
虚拟线程 = JVM 在“用户空间”里模拟的“轻量级线程”
特性 | 平台线程(传统线程) | 虚拟线程(Virtual Thread) |
---|---|---|
由谁创建 | 操作系统 | JVM |
数量上限 | 几千个 | 百万级 |
内存占用 | 约 1 MB/线程 | 几百字节起 |
切换成本 | 内核态切换,较重 | 纯用户态切换,极轻 |
是否会被阻塞 | 会 | 可以自动卸载(前提:不被 synchronized 或 JNI 钉住) |
什么叫"自动卸载"?
虚拟线程在执行 阻塞 I/O(如
socket.read()
)时,理论上会把当前任务从平台线程上“摘下来”,让这个平台线程去干别的活。这个过程叫 unmount(卸载)。
等 I/O 就绪后,再随便找一个空闲的平台线程把虚拟线程“挂回去”继续跑。
比喻
正常情况:虚拟线程像“乘客”,平台线程像“出租车”。
乘客打车去商场,路上堵车(阻塞 I/O)→ 乘客下车,出租车去接别的单。被钉住:乘客上车后把自己焊死在座位上(进入
synchronized
),出租车只能一直等他,别人打不了这辆出租车。