从零开始的实战教程 —— 搭建环境到代码运行,全程可跟做
JDK:21.0.11 (Oracle LTS)IDE:IDEA Educational 2021.3.1难度:入门 → 进阶
目录
- 环境准备:让 IDEA 正确识别 JDK 21
- 核心概念:同步与异步到底有什么区别
- 同步编程实战:感受"阻塞"的代价
- 多线程异步入门:Thread 与 Runnable
- CompletableFuture 深入:现代异步编程核心
- 虚拟线程 (JDK 21):轻量级并发新纪元
- 综合实战:模拟电商订单处理系统
1环境准备
在写第一行异步代码之前,我们需要确保 IDEA 能正确使用你电脑上的 JDK 21。
步骤 1:查看 JAVA_HOME 环境变量
可以输入
java -version javac -version echo %JAVA_HOME%来验证,预计输出
java version "21.0.11" 2026-04-21 LTS javac 21.0.11 C:\Program Files\Java\latest\jdk-21若不是21版本,可以通过修改环境变量的方式修改
操作路径:右键「此电脑」→ 属性 → 高级系统设置 → 环境变量
- 在「系统变量」区域,找到名为
JAVA_HOME的变量 - 点击「编辑」,将变量值改为:你的路径
- 找到名为
Path的系统变量,点击「编辑」 - 检查列表中是否有
%JAVA_HOME%\bin,如果没有,点击「新建」添加它 - 如果有旧的 条目,选中并删除它
- 连续点击「确定」保存所有窗口
步骤 2:打开 IDEA,准备创建项目
IDEA Educational 2021.3.1 发布于 2021 年,不会自动识别后来安装的 JDK 21。我们需要在创建项目时手动指定 JDK 路径。
- 打开 IntelliJ IDEA
- 在欢迎界面,直接点击New Project(新建项目)按钮
步骤 3:配置项目并创建
在 New Project 窗口中,按以下顺序操作:
- 左侧选择Java
- 在Project SDK下拉框中,如果看不到 JDK 21,点击下拉框 → 选择Add SDK → JDK...
- 在弹出的文件浏览窗口中,导航到
C:\Program Files\Java\latest\jdk-21文件夹,点击 OK - 现在 Project SDK 应显示为
21 - ... - 不要勾选任何 Additional Libraries(Archetype / Library),保持空白
- 点击Next
- 不要勾选"Create project from template",直接点击Next
- 项目名称填写:
JavaAsyncDemo - 项目位置(Location)选择一个你方便找到的目录,例如
C:\Users\侯丞\IdeaProjects\JavaAsyncDemo - 点击Finish
注意
IDEA 可能弹出一个提示 "Project SDK is not defined" 或 "No SDK"。如果出现,请确认上一步的 JDK 21 路径选择正确。路径必须是包含bin文件夹的那一层(即jdk-21本身),不要选到bin里面。
步骤 4验证项目 SDK 配置
项目创建完成后,确认 JDK 版本正确:
- 点击菜单File → Project Structure(快捷键 Ctrl+Alt+Shift+S)
- 左侧选择Project
- 确认Project SDK显示为
21 - 确认Project language level选择为
21 - Pattern matching for switch或SDK default - 点击 OK 关闭窗口
步骤 5创建包和主类
项目结构创建好后,我们来建立代码的组织结构。
- 在左侧 Project 面板中,展开
JavaAsyncDemo→src - 右键点击
src→New → Package - 输入包名:
com.demo,按回车 - 右键点击
com.demo→New → Java Class - 输入类名:
Main,按回车
IDEA 会自动生成以下代码框架:
package com.demo; public class Main { public static void main(String[] args) { // 在这里写代码 System.out.println("Hello, Async World!"); } }点击右上角绿色三角形运行按钮(或按 Shift+F10),如果控制台输出Hello, Async World!,说明环境配置完全正确。
JavaAsyncDemo/ ├── .idea/ ├── src/ │ └── com/ │ └── demo/ │ └── Main.java ← 主入口类 └── JavaAsyncDemo.iml后续所有章节的代码都将写在com.demo包下,每个示例新建一个独立的类。
2核心概念
在写代码之前,先理解一个根本问题:同步和异步到底是什么意思?
生活类比
同步 = 排队等
你去餐厅点餐。你点了一份炒饭,然后站在窗口前一直等,什么都做不了,直到炒饭做好。拿到炒饭后,你才能去点下一杯饮料。
特点:一件事做完,才做下一件。等待期间,你被"阻塞"了。
异步 = 拿号等
你去餐厅点餐。你点了一份炒饭,服务员给你一个号。你回到座位刷手机。等叫号时再去取餐。等待期间,你还可以点饮料、和朋友聊天。
特点:不需要干等。等待期间可以做别的事,等好了再回来处理结果。
程序中的同步与异步
在程序中,"等待"通常指等待某个耗时操作完成,比如:
- 从数据库读取数据(需要几十毫秒到几秒)
- 调用第三方 API(网络延迟,可能需要几秒)
- 读写文件(磁盘 I/O)
- 复杂计算任务(CPU 密集型)
下面的流程图展示了两种模式的核心区别:
图 2-1:同步 vs 异步执行流程对比
同步代码长什么样
同步代码就是最常见的"一行一行写"的代码,执行顺序从上到下:
/ 第1行执行完,才执行第2行 String data = fetchDataFromDB(); // 卡在这里等数据库返回... String result = process(data); // 等上一步完成才能开始 sendEmail(result); // 等上一步完成才能开始 System.out.println("全部完成");如果每个操作需要 2 秒,总共就需要 6 秒。而且这 6 秒里,主线程什么其他事都做不了。
异步代码长什么样
异步代码把"等待"这个动作交给别的线程去做,主线程继续往下走:
/ 把 fetchDB 交给另一个线程去做,主线程不等它 CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { return fetchDataFromDB(); }); // 主线程不等,继续做别的事 doSomethingElse(); // 等真正需要结果时,再等它 String data = future.join(); // 此时 fetchDB 可能已经做完了什么时候该用异步
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 简单工具脚本 | 同步 | 代码简单,性能不是瓶颈 |
| 调用多个外部 API | 异步 | 并行调用,总耗时大幅缩短 |
| Web 服务器处理请求 | 异步 | 一个请求等待时,可以处理其他请求 |
| 大批量数据处理 | 异步 + 多线程 | 利用多核 CPU 并行计算 |
| 学习 / 验证逻辑 | 同步 | 先保证逻辑正确,再考虑性能 |
学习建议
不要一上来就追求异步。先用同步代码把逻辑写对,然后找到性能瓶颈(通常是某个"等待"操作),再把它改成异步。这就是从"能用"到"好用"的进化过程。
3同步编程实战
我们先写一段同步代码,亲身体会"阻塞"是什么感受。这会成为理解异步价值的最佳铺垫。
创建同步演示类
- 右键
com.demo→New → Java Class - 输入类名:
SyncDemo - 输入以下完整代码
package com.demo; public class SyncDemo { // 模拟一个耗时操作:假装在查数据库 private static String fetchFromDB(String name, int millis) { try { Thread.sleep(millis); // 模拟耗时 } catch (InterruptedException e) { throw new RuntimeException(e); } return name + " 的数据已获取"; } public static void main(String[] args) { long start = System.currentTimeMillis(); System.out.println("=== 同步执行开始 ==="); // 三个耗时操作,依次执行 String r1 = fetchFromDB("用户信息", 1000); System.out.println(r1); String r2 = fetchFromDB("订单信息", 1000); System.out.println(r2); String r3 = fetchFromDB("积分信息", 1000); System.out.println(r3); long end = System.currentTimeMillis(); System.out.println("=== 同步执行结束,总耗时: " + (end - start) + "ms ==="); } }运行并观察结果
右键
SyncDemo类文件 →Run 'SyncDemo.main()'(或打开文件后按 Shift+F10)=== 同步执行开始 === 用户信息 的数据已获取 订单信息 的数据已获取 积分信息 的数据已获取 === 同步执行结束,总耗时: 3032ms ===关键发现
三个操作每个耗时 1 秒,总耗时约 3 秒。因为在同步模式下,第二个操作必须等第一个完成才能开始。如果这三个操作之间没有依赖关系,那这 2 秒的等待就是浪费。
问题分析
用下面的图来理解为什么同步模式慢:
图 3-1:同步执行 — 主线程全程被占用
主线程的时间线是一条直线,没有任何重叠。这就是同步模式的本质:串行执行,全程阻塞。接下来,我们用异步来优化它。
4多线程异步入门
异步编程最基础的实现方式就是多线程。我们先把上面的同步代码改成多线程版本。
方式一:继承 Thread 类
这是最直观的方式,但实际开发中用得较少(Java 不支持多重继承)。
- 新建类:
ThreadDemo - 输入以下代码
package com.demo; public class ThreadDemo { private static String fetchFromDB(String name, int millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { throw new RuntimeException(e); } return name + " 的数据已获取"; } // 继承 Thread,重写 run 方法 static class FetchTask extends Thread { private String name; private int millis; private String result; public FetchTask(String name, int millis) { this.name = name; this.millis = millis; } // run 方法里写线程要执行的逻辑 @Override public void run() { result = fetchFromDB(name, millis); System.out.println(Thread.currentThread().getName() + " -> " + result); } public String getResult() { return result; } } public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); System.out.println("=== 多线程执行开始 ==="); // 创建三个线程(还没开始运行) FetchTask t1 = new FetchTask("用户信息", 1000); FetchTask t2 = new FetchTask("订单信息", 1000); FetchTask t3 = new FetchTask("积分信息", 1000); // start() 启动线程,三个线程并发执行 t1.start(); t2.start(); t3.start(); // join() 让主线程等待这三个线程完成 t1.join(); t2.join(); t3.join(); long end = System.currentTimeMillis(); System.out.println("=== 多线程执行结束,总耗时: " + (end - start) + "ms ==="); } }注意:start() 不是 run()
调用
t1.start()会启动新线程并自动调用 run()。如果你直接写t1.run(),那不会创建新线程,只是在主线程里普通调用方法 —— 完全失去了多线程的意义。这是新手最常犯的错误。运行结果:
=== 多线程执行开始 === Thread-0 -> 用户信息 的数据已获取 Thread-1 -> 订单信息 的数据已获取 Thread-2 -> 积分信息 的数据已获取 === 多线程执行结束,总耗时: 1015ms ===效果对比
同步版耗时约 3000ms,多线程版约 1000ms。三个操作并行执行,总耗时约等于最慢的那个。这就是异步的价值。
方式二:实现 Runnable 接口(推荐)
继承 Thread 的方式有个问题:Java 只支持单继承,如果你的类已经继承了别的类,就不能再继承 Thread。
Runnable接口解决了这个问题。package com.demo; public class RunnableDemo { private static String fetchFromDB(String name, int millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { throw new RuntimeException(e); } return name + " 的数据已获取"; } public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); System.out.println("=== Runnable 执行开始 ==="); // 用数组保存结果 final String[] results = new String[3]; // 用 Lambda 创建 Runnable(比匿名内部类更简洁) Thread t1 = new Thread(() -> { results[0] = fetchFromDB("用户信息", 1000); System.out.println(results[0]); }); Thread t2 = new Thread(() -> { results[1] = fetchFromDB("订单信息", 1000); System.out.println(results[1]); }); Thread t3 = new Thread(() -> { results[2] = fetchFromDB("积分信息", 1000); System.out.println(results[2]); }); t1.start(); t2.start(); t3.start(); t1.join(); t2.join(); t3.join(); long end = System.currentTimeMillis(); System.out.println("=== 总耗时: " + (end - start) + "ms ==="); } }Thread vs Runnable 对比
对比项 继承 Thread 实现 Runnable 继承限制 不能再继承其他类 可以同时继承其他类 返回值 需要自己存 需要自己存 代码简洁度 需要写一个类 可用 Lambda,更简洁 线程复用 不支持 可配合线程池使用 推荐程度 不推荐 推荐 线程生命周期
每个线程从创建到消亡,会经历以下状态:
图 4-1:Java 线程生命周期状态转换
Thread 方式的局限
- 无法直接获取返回值:run() 方法返回 void,需要自己用变量存储结果
- 无法处理异常:子线程的异常不会传播到主线程
- 手动管理线程:创建和销毁线程开销大,没有复用机制
这些问题,CompletableFuture 全部解决了。这就是下一章的内容。
5CompletableFuture 深入
CompletableFuture是 Java 8 引入的异步编程工具,它能让你像写同步代码一样写异步代码,同时获得多线程的性能优势。
为什么需要 CompletableFuture
回顾上一章 Thread/Runnable 的痛点:
- 无法获取返回值 → CompletableFuture 可以
- 无法链式调用 → CompletableFuture 支持 then / whenComplete 等
- 无法组合多个异步任务 → CompletableFuture 支持 allOf / anyOf
- 无法处理异常 → CompletableFuture 支持 exceptionally / handle
基本用法:supplyAsync
新建类:CFDemo1
package com.demo; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; public class CFDemo1 { private static String fetchFromDB(String name, int millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { throw new RuntimeException(e); } return name + " 的数据已获取"; } public static void main(String[] args) { long start = System.currentTimeMillis(); System.out.println("=== CompletableFuture 基本用法 ==="); // supplyAsync:异步执行,有返回值 // 这行代码会立刻返回一个 CompletableFuture 对象 // 真正的 fetchFromDB 在另一个线程里执行 CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { return fetchFromDB("用户信息", 1000); }); // 主线程可以继续做其他事 System.out.println("主线程没有等待,继续执行..."); System.out.println("可以做其他事情..."); // 当需要结果时,调用 join() 等待(会阻塞直到完成) // join() 和 get() 的区别:join() 不抛 checked 异常 String result = future.join(); System.out.println("获取到结果: " + result); long end = System.currentTimeMillis(); System.out.println("总耗时: " + (end - start) + "ms"); } }运行结果:
=== CompletableFuture 基本用法 === 主线程没有等待,继续执行... 可以做其他事情... 获取到结果: 用户信息 的数据已获取 总耗时: 1012ms链式调用:thenApply / thenAccept / thenRun
CompletableFuture 最强大的特性之一是链式调用— 上一步的结果可以自动传给下一步
package com.demo; import java.util.concurrent.CompletableFuture; public class CFDemo2 { public static void main(String[] args) { System.out.println("=== 链式调用演示 ==="); CompletableFuture.supplyAsync(() -> { sleep(500); return "原始数据"; }) // thenApply:接收上一步的结果,返回新值 .thenApply(data -> { System.out.println("第一步收到: " + data); return data + " + 加工后"; }) // thenApply 再加工一次 .thenApply(data -> { System.out.println("第二步收到: " + data); return data.toUpperCase(); }) // thenAccept:接收结果,但不返回新值(消费结果) .thenAccept(result -> { System.out.println("最终结果: " + result); }) // thenRun:不关心结果,只执行一个动作 .thenRun(() -> { System.out.println("流程结束,收工!"); }) // join 等待整个链完成 .join(); } private static void sleep(int millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { throw new RuntimeException(e); } } }=== 链式调用演示 === 第一步收到: 原始数据 第二步收到: 原始数据 + 加工后 最终结果: 原始数据 + 加工后 流程结束,收工!三个链式方法对比
| 方法 | 接收上一步结果 | 返回新值 | 类比 |
|---|---|---|---|
thenApply | 能 | 能 | 加工流水线 |
thenAccept | 能 | 不能(返回 void) | 消费者 |
thenRun | 不能 | 不能 | 收尾动作 |
组合多个异步任务
这是 CompletableFuture 最实用的场景:同时发起多个异步任务,等它们全部完成。
package com.demo; import java.util.concurrent.CompletableFuture; public class CFDemo3 { private static String fetchFromDB(String name, int millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { throw new RuntimeException(e); } return name + " 的数据"; } public static void main(String[] args) { long start = System.currentTimeMillis(); System.out.println("=== 并行执行多个任务 ==="); // 同时启动三个异步任务 CompletableFuture<String> f1 = CompletableFuture.supplyAsync( () -> fetchFromDB("用户信息", 1000)); CompletableFuture<String> f2 = CompletableFuture.supplyAsync( () -> fetchFromDB("订单信息", 1000)); CompletableFuture<String> f3 = CompletableFuture.supplyAsync( () -> fetchFromDB("积分信息", 1000)); // allOf:等待全部完成 CompletableFuture.allOf(f1, f2, f3).join(); // 此时三个任务都已完成,取出结果 System.out.println(f1.join()); System.out.println(f2.join()); System.out.println(f3.join()); long end = System.currentTimeMillis(); System.out.println("总耗时: " + (end - start) + "ms"); // --- anyOf:只要任意一个完成就继续 --- System.out.println("\n=== anyOf 演示 ==="); CompletableFuture<String> fast = CompletableFuture.supplyAsync( () -> fetchFromDB("快速服务", 300)); CompletableFuture<String> slow = CompletableFuture.supplyAsync( () -> fetchFromDB("慢速服务", 2000)); Object firstResult = CompletableFuture.anyOf(fast, slow).join(); System.out.println("最先完成: " + firstResult); } }=== 并行执行多个任务 === 用户信息 的数据 订单信息 的数据 积分信息 的数据 总耗时: 1018ms === anyOf 演示 === 最先完成: 快速服务 的数据allOf vs anyOf
allOf(f1, f2, f3)— 等待全部完成。适合"所有数据都拿到了才能继续"的场景anyOf(f1, f2, f3)— 只要任意一个完成就继续。适合"多源查询,谁先返回用谁"的场景(比如查多个缓存)
异常处理
异步代码的异常处理和同步代码不同 —— 你不能用普通的 try-catch 包住整个链。CompletableFuture 提供了专门的异常处理方法。
package com.demo; import java.util.concurrent.CompletableFuture; public class CFDemo4 { public static void main(String[] args) { System.out.println("=== 异常处理演示 ==="); // 方式1: exceptionally — 出异常时提供默认值 String r1 = CompletableFuture.supplyAsync(() -> { if (true) throw new RuntimeException("数据库连接失败"); return "正常数据"; }) .exceptionally(ex -> { System.out.println("捕获异常: " + ex.getMessage()); return "降级数据(默认值)"; }) .join(); System.out.println("结果: " + r1); // 方式2: handle — 无论成功失败都会执行 String r2 = CompletableFuture.supplyAsync(() -> { throw new RuntimeException("网络超时"); }) .handle((result, ex) -> { if (ex != null) { System.out.println("handle 捕获: " + ex.getMessage()); return "兜底数据"; } return result + " 处理完毕"; }) .join(); System.out.println("结果: " + r2); // 方式3: whenComplete — 类似 finally,不能修改结果 CompletableFuture.supplyAsync(() -> "成功数据") .whenComplete((result, ex) -> { if (ex == null) { System.out.println("whenComplete 正常完成: " + result); } else { System.out.println("whenComplete 出错了: " + ex.getMessage()); } }) .join(); } }三种异常处理方法对比
| 方法 | 只在异常时执行 | 能修改返回值 | 使用场景 |
|---|---|---|---|
exceptionally | 是 | 能(返回默认值) | 提供降级数据 |
handle | 否(成功失败都执行) | 能 | 统一处理成功和失败 |
whenComplete | 否 | 不能 | 记录日志、清理资源 |
使用自定义线程池
默认情况下,supplyAsync使用 Java 的ForkJoinPool.commonPool()。但在生产环境中,你应该使用自定义线程池来更好地控制并发数。
package com.demo; import java.util.concurrent.*; public class CFDemo5 { public static void main(String[] args) { // 创建自定义线程池 // 核心线程数 = 4,最大线程数 = 8 // 队列容量 = 100,空闲 60 秒回收 ExecutorService executor = new ThreadPoolExecutor( 4, // corePoolSize 8, // maxPoolSize 60L, // keepAliveTime TimeUnit.SECONDS, // unit new LinkedBlockingQueue<>(100), // workQueue new ThreadFactory() { private int count = 0; @Override public Thread newThread(Runnable r) { return new Thread(r, "my-pool-" + count++); } } ); long start = System.currentTimeMillis(); // 用自定义线程池执行异步任务 CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> { sleep(1000); return "任务1完成"; }, executor); CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> { sleep(1000); return "任务2完成"; }, executor); CompletableFuture.allOf(f1, f2).join(); System.out.println(f1.join()); System.out.println(f2.join()); long end = System.currentTimeMillis(); System.out.println("总耗时: " + (end - start) + "ms"); // 记得关闭线程池! executor.shutdown(); } private static void sleep(int millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { throw new RuntimeException(e); } } }重要提醒
自定义线程池用完后必须调用shutdown(),否则线程池中的线程不会停止,程序不会退出。这是初学者经常遇到的"程序运行完了但一直不结束"的问题。
6虚拟线程
虚拟线程(Virtual Thread)是 JDK 21 引入的重磅特性。它让 Java 像 Go 的 goroutine 一样,能轻松创建数万个线程而不崩溃。
为什么需要虚拟线程
传统线程(平台线程)的问题:
- 每个平台线程对应一个操作系统线程,创建成本高(约 1MB 内存)
- 一台普通服务器最多创建几千个平台线程
- 线程多了,CPU 在线程切换上的开销也急剧增加
虚拟线程的优势:
- 由 JVM 管理,不直接对应 OS 线程,创建成本极低(约几 KB)
- 可以轻松创建数万个虚拟线程
- 代码写法和传统线程几乎一模一样,学习成本低
图 6-1:平台线程 vs 虚拟线程的承载模型
创建虚拟线程的两种方式
新建类:VirtualThreadDemo
package com.demo; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.stream.IntStream; public class VirtualThreadDemo { public static void main(String[] args) throws InterruptedException { System.out.println("=== 虚拟线程演示 ==="); // 方式1: Thread.ofVirtual() 直接创建 Thread vt1 = Thread.ofVirtual().start(() -> { sleep(500); System.out.println("虚拟线程1 执行完毕 - " + Thread.currentThread()); }); // 方式2: startVirtualThread (更简洁) Thread vt2 = Thread.startVirtualThread(() -> { sleep(500); System.out.println("虚拟线程2 执行完毕 - " + Thread.currentThread()); }); // 等待两个虚拟线程完成 vt1.join(); vt2.join(); System.out.println("\n=== 批量创建 10000 个虚拟线程 ==="); long start = System.currentTimeMillis(); // 方式3: newVirtualThreadPerTaskExecutor // 这是推荐的方式:每个任务分配一个虚拟线程 try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { // 提交 10000 个任务 var futures = IntStream.range(0, 10000) .mapToObj(i -> executor.submit(() -> { sleep(100); return i; })) .toList(); // 等待所有任务完成 int completed = 0; for (var f : futures) { f.join(); completed++; } System.out.println("完成数量: " + completed); } // try-with-resources 自动关闭 executor long end = System.currentTimeMillis(); System.out.println("10000 个虚拟线程总耗时: " + (end - start) + "ms"); // 对比:用平台线程创建 10000 个 System.out.println("\n=== 对比:10000 个平台线程 ==="); long start2 = System.currentTimeMillis(); try (ExecutorService executor2 = Executors.newCachedThreadPool()) { var futures2 = IntStream.range(0, 10000) .mapToObj(i -> executor2.submit(() -> { sleep(100); return i; })) .toList(); for (var f : futures2) { f.join(); } } long end2 = System.currentTimeMillis(); System.out.println("10000 个平台线程总耗时: " + (end2 - start2) + "ms"); } private static void sleep(int millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { throw new RuntimeException(e); } } }运行结果说明
虚拟线程版本:10000 个任务在约 100-300ms 内全部完成(因为它们是并行的,每个只 sleep 100ms)
平台线程版本:耗时会明显更长,因为创建 10000 个平台线程会消耗大量内存和 CPU 时间用于线程切换。
IDEA 可能显示黄色警告
IDEA Educational 2021.3.1 不认识Thread.ofVirtual()和Executors.newVirtualThreadPerTaskExecutor(),可能标红或显示警告。这不影响编译和运行,只要你选择了 JDK 21 作为项目 SDK。直接运行即可。
平台线程 vs 虚拟线程对比
| 对比项 | 平台线程 (传统) | 虚拟线程 (JDK 21+) |
|---|---|---|
| 内存占用 | 约 1MB / 线程 | 约几 KB / 线程 |
| 建议数量 | 数百到数千 | 数万到数百万 |
| 创建成本 | 高(涉及 OS 调用) | 极低(JVM 管理) |
| 适用场景 | CPU 密集型计算 | I/O 密集型(网络、数据库) |
| 代码写法 | 传统 Thread / Runnable | 完全兼容,无需改动 |
| 引入版本 | JDK 1.0 | JDK 21 (预览: JDK 19-20) |
使用建议
虚拟线程的最佳实践
- I/O 密集型任务用虚拟线程(网络请求、数据库查询、文件读写)
- CPU 密集型任务仍用平台线程(大量数学计算、加密解密)
- 不要池化虚拟线程 —— 用完就扔,JVM 会自动回收
- 在虚拟线程里不要使用
synchronized,改用ReentrantLock(JDK 21+ 对此有优化,但 Lock 更安全)
7综合实战
最后,我们用一个贴近实际的案例来综合运用前面学到的知识。场景:模拟一个电商订单查询系统,需要并行调用三个微服务获取数据,然后汇总返回。
创建综合演示类
package com.demo; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicLong; public class OrderSystemDemo { // ========== 模拟三个微服务 ========== /** 模拟用户服务(耗时约 500ms) */ static String getUserService(String userId) { sleep(500); return "用户[(" + userId + ")张三, VIP]"; } /** 模拟订单服务(耗时约 800ms) */ static String getOrderService(String userId) { sleep(800); return "订单[ORD-2024-001, 金额 ¥299.00]"; } /** 模拟物流服务(耗时约 600ms) */ static String getLogisticsService(String orderId) { sleep(600); return "物流[顺丰速运, 预计明天送达]"; } // ========== 方式一:同步查询 ========== static void syncQuery(String userId) { long start = System.currentTimeMillis(); String user = getUserService(userId); String order = getOrderService(userId); String logistics = getLogisticsService("ORD-001"); String result = user + " | " + order + " | " + logistics; long elapsed = System.currentTimeMillis() - start; System.out.println("[同步] " + result); System.out.println("[同步] 耗时: " + elapsed + "ms\n"); } // ========== 方式二:CompletableFuture 并行查询 ========== static void asyncQueryWithCF(String userId) { long start = System.currentTimeMillis(); CompletableFuture<String> userFuture = CompletableFuture.supplyAsync( () -> getUserService(userId)); CompletableFuture<String> orderFuture = CompletableFuture.supplyAsync( () -> getOrderService(userId)); // 物流查询依赖订单结果(先拿到订单号再查物流) CompletableFuture<String> logisticsFuture = orderFuture .thenCompose(order -> CompletableFuture.supplyAsync( () -> getLogisticsService("ORD-001"))); // 等待全部完成,然后合并结果 CompletableFuture.allOf(userFuture, orderFuture, logisticsFuture) .thenApply(v -> { String result = userFuture.join() + " | " + orderFuture.join() + " | " + logisticsFuture.join(); return result; }) .thenAccept(result -> { long elapsed = System.currentTimeMillis() - start; System.out.println("[CompletableFuture] " + result); System.out.println("[CompletableFuture] 耗时: " + elapsed + "ms\n"); }) .join(); } // ========== 方式三:虚拟线程并行查询 ========== static void asyncQueryWithVirtualThread(String userId) throws Exception { long start = System.currentTimeMillis(); // 用数组保存结果(简单方案,适合演示) final String[] user = new String[1]; final String[] order = new String[1]; final String[] logistics = new String[1]; try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { Future<String> userF = executor.submit(() -> { user[0] = getUserService(userId); return user[0]; }); Future<String> orderF = executor.submit(() -> { order[0] = getOrderService(userId); return order[0]; }); // 物流依赖订单,先等订单完成 orderF.get(); Future<String> logisticsF = executor.submit(() -> { logistics[0] = getLogisticsService("ORD-001"); return logistics[0]; }); userF.get(); logisticsF.get(); } String result = user[0] + " | " + order[0] + " | " + logistics[0]; long elapsed = System.currentTimeMillis() - start; System.out.println("[虚拟线程] " + result); System.out.println("[虚拟线程] 耗时: " + elapsed + "ms\n"); } // ========== Main ========== public static void main(String[] args) throws Exception { String userId = "U001"; System.out.println("====== 电商订单查询系统 ======\n"); System.out.println("--- 方式一:同步查询 ---"); syncQuery(userId); System.out.println("--- 方式二:CompletableFuture ---"); asyncQueryWithCF(userId); System.out.println("--- 方式三:虚拟线程 ---"); asyncQueryWithVirtualThread(userId); System.out.println("====== 查询结束 ======"); } private static void sleep(int millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { throw new RuntimeException(e); } }</code></pre> 运行结果分析 ====== 电商订单查询系统 ====== --- 方式一:同步查询 --- [同步] 用户[(U001)张三, VIP] | 订单[ORD-2024-001, 金额 ¥299.00] | 物流[顺丰速运, 预计明天送达] [同步] 耗时: 1903ms --- 方式二:CompletableFuture --- [CompletableFuture] 用户[(U001)张三, VIP] | 订单[ORD-2024-001, 金额 ¥299.00] | 物流[顺丰速运, 预计明天送达] [CompletableFuture] 耗时: 1412ms --- 方式三:虚拟线程 --- [虚拟线程] 用户[(U001)张三, VIP] | 订单[ORD-2024-001, 金额 ¥299.00] | 物流[顺丰速运, 预计明天送达] [虚拟线程] 耗时: 1408ms ====== 查询结束 ====== 性能对比解读 方式 耗时 原因分析 同步 ~1900ms 用户(500) + 订单(800) + 物流(600) = 串行相加 CompletableFuture ~1400ms 用户和订单并行(max=800),物流依赖订单后执行(600),总计 800+600=1400 虚拟线程 ~1400ms 同样的并行逻辑,耗时接近 关键发现 CompletableFuture 和虚拟线程的性能几乎相同,因为并行逻辑一样。它们的区别在于: CompletableFuture 更擅长任务编排(thenCompose, allOf, anyOf 等),适合复杂依赖关系 虚拟线程 更擅长高并发 I/O,代码写法更简单直接,适合大量并发请求 在实际项目中,两者可以结合使用:用虚拟线程执行单个异步任务,用 CompletableFuture 编排多个任务的依赖关系。 知识总结 图 7-1:Java 异步编程知识体系总览