Java 同步与异步编程

从零开始的实战教程 —— 搭建环境到代码运行,全程可跟做

JDK:21.0.11 (Oracle LTS)IDE:IDEA Educational 2021.3.1难度:入门 → 进阶

目录

  1. 环境准备:让 IDEA 正确识别 JDK 21
  2. 核心概念:同步与异步到底有什么区别
  3. 同步编程实战:感受"阻塞"的代价
  4. 多线程异步入门:Thread 与 Runnable
  5. CompletableFuture 深入:现代异步编程核心
  6. 虚拟线程 (JDK 21):轻量级并发新纪元
  7. 综合实战:模拟电商订单处理系统

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版本,可以通过修改环境变量的方式修改

操作路径:右键「此电脑」→ 属性 → 高级系统设置 → 环境变量

  1. 在「系统变量」区域,找到名为JAVA_HOME的变量
  2. 点击「编辑」,将变量值改为:你的路径
  3. 找到名为Path的系统变量,点击「编辑」
  4. 检查列表中是否有%JAVA_HOME%\bin,如果没有,点击「新建」添加它
  5. 如果有旧的 条目,选中并删除它
  6. 连续点击「确定」保存所有窗口

步骤 2:打开 IDEA,准备创建项目

IDEA Educational 2021.3.1 发布于 2021 年,不会自动识别后来安装的 JDK 21。我们需要在创建项目时手动指定 JDK 路径。

  1. 打开 IntelliJ IDEA
  2. 在欢迎界面,直接点击New Project(新建项目)按钮

步骤 3:配置项目并创建

在 New Project 窗口中,按以下顺序操作:

  1. 左侧选择Java
  2. Project SDK下拉框中,如果看不到 JDK 21,点击下拉框 → 选择Add SDK → JDK...
  3. 在弹出的文件浏览窗口中,导航到C:\Program Files\Java\latest\jdk-21文件夹,点击 OK
  4. 现在 Project SDK 应显示为21 - ...
  5. 不要勾选任何 Additional Libraries(Archetype / Library),保持空白
  6. 点击Next
  7. 不要勾选"Create project from template",直接点击Next
  8. 项目名称填写:JavaAsyncDemo
  9. 项目位置(Location)选择一个你方便找到的目录,例如C:\Users\侯丞\IdeaProjects\JavaAsyncDemo
  10. 点击Finish

注意

IDEA 可能弹出一个提示 "Project SDK is not defined" 或 "No SDK"。如果出现,请确认上一步的 JDK 21 路径选择正确。路径必须是包含bin文件夹的那一层(即jdk-21本身),不要选到bin里面。

步骤 4验证项目 SDK 配置

项目创建完成后,确认 JDK 版本正确:

  1. 点击菜单File → Project Structure(快捷键 Ctrl+Alt+Shift+S)
  2. 左侧选择Project
  3. 确认Project SDK显示为21
  4. 确认Project language level选择为21 - Pattern matching for switchSDK default
  5. 点击 OK 关闭窗口

步骤 5创建包和主类

项目结构创建好后,我们来建立代码的组织结构。

  1. 在左侧 Project 面板中,展开JavaAsyncDemosrc
  2. 右键点击srcNew → Package
  3. 输入包名:com.demo,按回车
  4. 右键点击com.demoNew → Java Class
  5. 输入类名: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同步编程实战

我们先写一段同步代码,亲身体会"阻塞"是什么感受。这会成为理解异步价值的最佳铺垫。

创建同步演示类

  1. 右键com.demoNew → Java Class
  2. 输入类名:SyncDemo
  3. 输入以下完整代码
    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 不支持多重继承)。

  4. 新建类:ThreadDemo
  5. 输入以下代码
    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() + " -&gt; " + 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(() -&gt; { results[0] = fetchFromDB("用户信息", 1000); System.out.println(results[0]); }); Thread t2 = new Thread(() -&gt; { results[1] = fetchFromDB("订单信息", 1000); System.out.println(results[1]); }); Thread t3 = new Thread(() -&gt; { 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&lt;String&gt; future = CompletableFuture.supplyAsync(() -&gt; { 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(() -&gt; { sleep(500); return "原始数据"; }) // thenApply:接收上一步的结果,返回新值 .thenApply(data -&gt; { System.out.println("第一步收到: " + data); return data + " + 加工后"; }) // thenApply 再加工一次 .thenApply(data -&gt; { System.out.println("第二步收到: " + data); return data.toUpperCase(); }) // thenAccept:接收结果,但不返回新值(消费结果) .thenAccept(result -&gt; { System.out.println("最终结果: " + result); }) // thenRun:不关心结果,只执行一个动作 .thenRun(() -&gt; { 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&lt;String&gt; f1 = CompletableFuture.supplyAsync( () -&gt; fetchFromDB("用户信息", 1000)); CompletableFuture&lt;String&gt; f2 = CompletableFuture.supplyAsync( () -&gt; fetchFromDB("订单信息", 1000)); CompletableFuture&lt;String&gt; f3 = CompletableFuture.supplyAsync( () -&gt; 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&lt;String&gt; fast = CompletableFuture.supplyAsync( () -&gt; fetchFromDB("快速服务", 300)); CompletableFuture&lt;String&gt; slow = CompletableFuture.supplyAsync( () -&gt; 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(() -&gt; { if (true) throw new RuntimeException("数据库连接失败"); return "正常数据"; }) .exceptionally(ex -&gt; { System.out.println("捕获异常: " + ex.getMessage()); return "降级数据(默认值)"; }) .join(); System.out.println("结果: " + r1); // 方式2: handle — 无论成功失败都会执行 String r2 = CompletableFuture.supplyAsync(() -&gt; { throw new RuntimeException("网络超时"); }) .handle((result, ex) -&gt; { if (ex != null) { System.out.println("handle 捕获: " + ex.getMessage()); return "兜底数据"; } return result + " 处理完毕"; }) .join(); System.out.println("结果: " + r2); // 方式3: whenComplete — 类似 finally,不能修改结果 CompletableFuture.supplyAsync(() -&gt; "成功数据") .whenComplete((result, ex) -&gt; { 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&lt;&gt;(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&lt;String&gt; f1 = CompletableFuture.supplyAsync(() -&gt; { sleep(1000); return "任务1完成"; }, executor); CompletableFuture&lt;String&gt; f2 = CompletableFuture.supplyAsync(() -&gt; { 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(() -&gt; { sleep(500); System.out.println("虚拟线程1 执行完毕 - " + Thread.currentThread()); }); // 方式2: startVirtualThread (更简洁) Thread vt2 = Thread.startVirtualThread(() -&gt; { 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 -&gt; executor.submit(() -&gt; { 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 -&gt; executor2.submit(() -&gt; { 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.0JDK 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&lt;String&gt; userFuture = CompletableFuture.supplyAsync( () -&gt; getUserService(userId)); CompletableFuture&lt;String&gt; orderFuture = CompletableFuture.supplyAsync( () -&gt; getOrderService(userId)); // 物流查询依赖订单结果(先拿到订单号再查物流) CompletableFuture&lt;String&gt; logisticsFuture = orderFuture .thenCompose(order -&gt; CompletableFuture.supplyAsync( () -&gt; getLogisticsService("ORD-001"))); // 等待全部完成,然后合并结果 CompletableFuture.allOf(userFuture, orderFuture, logisticsFuture) .thenApply(v -&gt; { String result = userFuture.join() + " | " + orderFuture.join() + " | " + logisticsFuture.join(); return result; }) .thenAccept(result -&gt; { 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&lt;String&gt; userF = executor.submit(() -&gt; { user[0] = getUserService(userId); return user[0]; }); Future&lt;String&gt; orderF = executor.submit(() -&gt; { order[0] = getOrderService(userId); return order[0]; }); // 物流依赖订单,先等订单完成 orderF.get(); Future&lt;String&gt; logisticsF = executor.submit(() -&gt; { 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 异步编程知识体系总览