
JUC编程
10 线程池(重点)
1、Executors
Executors是Java并发包中,用来"快速创建和管理线程池"的工具类。它本身并不执行任务,只是一个"线程池工厂"。
Executors是JDK提供的线程池工厂类,用于快速创建常见的ExecutorService实现,它通过封装ThreadPoolExecutor简化并发编程,但其默认实现存在无界队列或无限线程的问题,在生产环境中通常建议直接使用ThreadPoolExecutor自定义线程池参数。
三大方法
一、Executors.newSingleThreadExecutor()
-
作用
所有任务严格按照提交顺序,一个一个地执行
- 同一时间只会有一个线程
- 不会并发执行
- 比 new Thread().start() 更安全(县城一场会自动补新线程)
-
底层原理(重要)
源码等价于
javanew ThreadPoolExecutor( 1, // corePoolSize 1, // maximumPoolSize 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>() // 无界队列 );- 核心线程数=最大线程数=1
- 使用无界队列LinkedBlockingQueue
- 永远只有一个工作线程
- 任务堆积在队列里,排队执行
- 线程异常?线程池会自动创建一个新的线程继续执行后续任务
-
典型使用场景:需要顺序性保证的任务
- 日志写入(避免乱序)
- 文件操作
- 数据库单线程更新
- 事件穿行处理(消息消费)
-
风险点:**任务过多会OOM。因为LinkedBlockingQueue是无界队列,**允许的请求队列长度为 Integer.MAX_VALUE(约21亿),会堆积大量请求。
二、Executor.newFixedThreadPool(n)
-
作用
固定数量的线程并发执行任务
- 同时最多n个任务执行
- 超过的任务进入队列等待
-
底层原理
源码等价于:
javanew ThreadPoolExecutor( n, // corePoolSize n, // maximumPoolSize 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()// 无界队列 );- 线程数永远是n
- 任务多了就进队列
- 不会创建超过n个线程
-
典型使用场景
CPU密集型任务
- 图片处理
- 视频转码
- 数学计算
- 一般经验:线程数 ≈ CPU核心数
-
风险点:仍然可能OOM。因为LinkedBlockingQueue是无界队列,允许的请求队列长度为 Integer.MAX_VALUE(约21亿),会堆积大量请求。
三、Executor.newCachedThreadPool()
-
作用
来任务就创建线程,用完就回收
- 线程数几乎无限
- 适合大量短生命周期任务
-
底层原理
源码等价于:
javanew ThreadPoolExecutor( 0, // corePoolSize Integer.MAX_VALUE, // maximumPoolSize 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>() ); -
典型使用场景
I/O密集型、短任务
- 网络请求
- RPC调用
- 异步日志
- 突发流量
-
风险点:极易线程爆炸 ,允许的创建线程数量为
Integer.MAX_VALUE(约21亿),可能会创建大量的线程。大量请求 → 大量线程 → CPU飙升 → OOM/系统崩溃
三种方法对比
| 方法 | 线程数量 | 队列 | 是否可能OOM | 适合场景 |
|---|---|---|---|---|
| Single | 1 | LinkedBlockingQueue | ✅ | 顺序执行 |
| Fixed | n | LinkedBlockingQueue | ✅ | CPU密集 |
| Cache | 不限 | SynchronousQueue | ✅ | I/O密集 |
为什么不推荐直接用Executors?
阿里Java开发手册明确禁止。原因:所有Executors默认实现,要么无限队列。要么无限线程。
正确姿势
java
new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(capacity),
new ThreadPoolExecutor.AbortPolicy()
);
- 可控
- 可预期
- 不会炸内存
总结
newSingleThreadExecutor 和 newFixedThreadPool 本质都是使用无界队列 ,存在 OOM 风险;newCachedThreadPool 使用 SynchronousQueue,线程数不受限,存在线程爆炸风险,实际生产中应通过 ThreadPoolExecutor 自定义参数来创建线程池。
2、ThreadPoolExecutor
ThreadPoolExecutor是Java中线程池的核心实现类,通过"核心线程 + 最大线程 + 队列 + 拒绝从策略"的组合,实现对并发任务执行的"限流、复用、调度和保护"。
一、ThreadPoolExecutor的核心作用
-
统一管理线程生命周期,避免频繁
new Thread()- 创建
- 复用
- 回收
- 异常恢复
-
控制并发规模(核心),不是来多少任务就执行多少。
通过
corePoolSize,maximumPoolSize和workQueue对系统施加上限。 -
提供"背压 + 保护机制"
- 队列满了怎么办?
- 线程到上限了怎么半?
交给RejectExecutionHandler
二、构造方法七大参数解释(重点)
java
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
-
corePoolSize 核心线程数
线程池希望长期保持的线程数量:若当前线程数 < corePoolSize,则直接创建新线程执行任务
- 即使空闲,也不会被回收
- 优先创建
当
execute(task):- 如果当前工作线程 < corePoolSize
- 直接新建线程(核心线程)来执行这个任务
- 不会先进队列
- 如果当前工作线程数 ≥ corePoolSize
- 不在因为来新任务就加线程,转而先尝试入队(由
workQueue决定)
- 不在因为来新任务就加线程,转而先尝试入队(由
-
maximumPoolSize 最大线程数
线程池能创建的线程总数上限 (包含核心线程 + 非核心线程),只有在"队列满了塞不进去"的情况下,线程池才会尝试把线程数从
corePoolSize继续扩到maximumPoolSize。- 空闲时,超过corePoolSize的那部分非核心线程会在超过
keepAliveTime后被回收
当
当前线程数 ≥ corePoolSize且workQueue.offer(task)失败(通常意味着队列满了)时:- 如果
当前线程数 < maximumPoolSize- 新建非核心线程来执行任务(扩容)
- 否则
- 触发拒绝策略 handler
- 空闲时,超过corePoolSize的那部分非核心线程会在超过
-
keepAliveTime + unit 线程存活时间
含义:非核心线程的空闲存活时间
- 超时后会被回收
- 减少资源占用
如果开启
allowCoreThreadTimeOut(true),则核心线程也可回收 -
workQueue 任务队列(核心之一)
含义:存放"等待执行任务"的缓冲区。常见实现:
队列 特点 ArrayBlockingQueue 有界 LinkedBlockingQueue 可无界 SynchronousQueue 不存任务 PriorityBlockingQueue 带优先级 队列类型决定线程池行为
-
threadFactory 线程工厂
含义:定义线程"如何被创建"
用途:
- 线程命名
- 设置是否守护线程
- 设置优先级
- 统一异常处理器
-
handler 拒绝策略
含义:当前任务无法被执行时的处理方式
线程池的处理能力是有限的(线程数有上限、队列容量有限)。当任务提交速度 长期 > 线程池处理速度,就会出现"挤爆":
- 线程已经到
maximumPoolSize - 队列也满了(或队列根本不存任务,比如
SynchronousQueue)
内置策略:
策略 行为 特性 AbortPolicy 抛出异常(默认) 不能丢,必须明确失败并报警 CallerRunsPolicy 调用者执行 尽量不丢,但可以变慢、需要背压 DiscardPolicy 直接丢弃 可丢,丢了也没大影响 DiscardOldestPolicy 丢弃最老任务 只关心最新,旧的没价值 四种拒绝策略
-
AbortPolicy
行为:直接抛
RejectedExecutionException。✅ 优点:快速暴露问题,防止"默默丢任务"
❌ 缺点:如果上层没捕获,可能把业务线程打崩/请求失败
适用:任务不能丢、宁可失败也要报警(金融扣款、订单创建、关键写入)。
-
CallerRunsPolicy
行为:让"提交任务的线程"自己去执行该任务(而不是线程池)。
✅ 优点:天然背压:提交者被迫干活 → 提交速度下降 → 系统缓一口气
✅ 优点:不丢任务(除非线程池关闭)
❌ 缺点:可能把调用方拖慢,甚至拖垮关键线程(比如 Netty IO 线程、Servlet 容器线程、UI 线程)
❌ 缺点:会导致响应时间飙升、线程栈变复杂
适用:允许变慢但尽量不丢任务,且"提交者线程"不是关键线程(或你能接受它变慢)。例如:异步日志、非关键后台任务。
-
DiscardPolicy
行为:直接丢弃任务,什么都不做、也不抛异常。
✅ 优点:最省事,系统最稳(不额外制造压力)
❌ 缺点:悄无声息丢任务,排查极难,业务可能"少执行了你还不知道"
适用:任务可丢且有替代(比如:刷新缓存的重复任务、统计上报、埋点、低价值异步通知),并且你有监控能看到丢弃率。
-
DiscardOldestPolicy
行为:丢弃队列里"最老的那个任务",再尝试把当前任务入队。
✅ 优点:让"新任务"更容易被处理,适合"只关心最新"
❌ 缺点:会导致队列前面的任务饿死/丢失,而且丢的是哪个你不一定意识到
❌ 缺点:在某些负载下可能"循环丢弃",造成抖动
适用:更关心最新数据(例如:状态刷新、UI 渲染更新、行情/传感器数据的刷新任务),旧的执行了也没意义。
- 线程已经到
总结
线程池承载能力(吞吐极限) = maximumPoolSize + workQueue.capacity
ThreadPoolExecutor 是 Java 线程池的核心实现类,通过 corePoolSize、maximumPoolSize 和 workQueue 控制并发规模,通过 keepAliveTime 回收空闲线程,并在资源耗尽时通过 RejectedExecutionHandler 实现系统保护,从而实现高效、可控、可扩展的并发任务执行。
3、最大线程数应该如何定义?
一、CPU密集型(CPU-bound)
任务的大部分时间都在做计算,占着 CPU 不放。
- 线程几乎一直在计算
- 很少阻塞或等待
- 如:数学计算、图像/视频编码、加密解密、机器学习推理、排序/搜索/大量循环运算......
常见经验公式:最大线程数 ≈ CPU 核数 或 CPU 核数 + 1
二、IO密集型
任务的大部分时间在"等外部资源",CPU反而是空着的。
- 线程经常阻塞
- CPU利用率不高,但线程没法干别的
- 如:
- 读写磁盘、网络请求(HTTP / RPC)、数据库访问、文件上传下载、远程服务调用
常用经验公式:最大线程数 ≈ CPU 核数 / (1 - 阻塞系数)
- 阻塞系数 ≈ IO等待时间占比
- 0.8(80%等待) → 8/0.2 = 40
- 0.9(90%等待) → 8/0.1 = 80
三、设置最大线程数时,"密集型"的真正含义
你在设置 maximumPoolSize 时,实际上是在回答这两个问题:
- 线程是在用CPU,还是在等待资源?
- CPU空闲时间能不能被别的线程利用?
| 类型 | 线程状态 | maxPoolSize设置 |
|---|---|---|
| CPU密集型 | 一直RUNNING | 上限 ≈ CPU核数 |
| IO密集型 | 大量WAITTING | 上限 >> CPU核数 |
总结
CPU 密集型任务主要消耗计算资源,线程几乎不阻塞,因此线程数不宜超过 CPU 核心数,否则会因频繁上下文切换降低性能;
IO 密集型任务大部分时间在等待外部资源,CPU 经常空闲,通过增加线程数可以提高 CPU 利用率,因此最大线程数通常远大于 CPU 核心数。
11 四大函数式接口(重点,必须掌握)
新时代程序员技能:lambda表达式、链式编程、函数式接口、Stream流式计算
| 接口 | 作用 | 直觉理解 |
|---|---|---|
| Function<T, R> | 把T变成R | 加工、映射、转换 |
| Predicate<T> | 判断T是不是 | 条件、过滤、if |
| Consumer<T> | 消费T,不返回 | 干活、使用、输出 |
| Supplier<T> | 提供T,没有输入 | 生产、获取、懒加载 |
口诀:Function 做转换,Predicate 做判断,Consumer 只干活,Supplier 只给货
一、Function<T, R>------把一个东西变成另一个
-
定义
java@FunctionalInterface public interface Function<T, R> { R apply(T t); }- 有输入
- 有输出
- 重点是"转换关系"
二、Predicate<T>------判断一个条件,真 or 假
-
定义
java@FunctionalInterface public interface Predicate<T> { boolean test(T t); }- 有输入
- 输出永远是boolean
- 用于判断/过滤/条件
三、Consumer<T>------只使用,不返回
-
定义
java@FunctionalInterface public interface Consumer<T> { void accept(T t); }- 有输入
- 没有返回值
- 重点是副作用
四、Supplier<T>------只负责提供,不接受输入
-
定义
java@FunctionalInterface public interface Supplier<T> { T get(); }- 没有输入
- 有输出
- 常用于延迟创建/懒加载
12 Stream流式计算
一、什么是Stream
Stream 是一种对数据进行流式计算的抽象,不存储数据,只描述计算过程
Stream的三个关键特征
| 特征 | 说明 |
|---|---|
| 不存数据 | 数据仍在Collection/数组中 |
| 惰性执行 | 不调用终止操作就不执行 |
| 一次性 | Stream只能使用一次 |
Stream ≠ Colletion(非常重要)
| 对比 | Collection | Stream |
|---|---|---|
| 是否存储数据 | ✅ | ❌ |
| 是否可复用 | ✅ | ❌ |
| 关注点 | 数据 | 计算 |
| 编程风格 | 命令式(给出步骤) | 声明式(只要结果) |
Stream更像"流水线设计图",不是仓库
二、什么是流式计算
流式计算的思想
数据像水一样,一个元素一个元素地流过一系列操作
markdown
元素1 → filter → map → forEach
元素2 → filter → map → forEach
而不是:
markdown
全部 filter → 全部 map → 全部 forEach
这种方式叫:
- 流水线执行
- 垂直执行
- Pipeline Fusion
三、Stream的整体结构
Stream一定包含三部分
markdown
数据源 → 中间操作(0~N) → 终止操作(1)
实例
java
list.stream() // 数据源
.filter(x -> x > 3) // 中间操作
.map(x -> x * 2) // 中间操作
.forEach(System.out::println); // 终止操作
- 没有终止操作,整个Stream什么都不会执行
四、Stream常用方法
1、创建Stream(数据源)
-
Collection.stream()
javalist.stream(); list.parallelStream();原理:
- 不复制数据
- 底层基于底层基于
Spliterator - 并行流通过ForkJoinPool执行
-
Stream.of()
javaStream.of(1, 2, 3);原理:
- 基于数组创建流
-
Stream.generate/iterate(无限流)
javaStream.generate(Math::random) .limit(3) .forEach(System.out::println);javaStream.iterate(0, x -> x + 1) .limit(5) .forEach(System.out::println);- 无限流必须配合
limit使用
- 无限流必须配合
2、中间操作(返回Stream,惰性)
中间操作的本质:
给 Stream 加一层"处理规则"
-
filter------过滤
javastream.filter(x -> x > 10);- 使用
Predicate<T> - 不满足条件的元素直接丢弃
- 无状态操作
- 使用
-
map------转换
javastream.map(x -> x * 2);- 使用
Function<T, R> - 一对一映射
- 不修改原数据
- 使用
-
flatMap------扁平化
javaList<List<Integer>> lists = ... lists.stream() .flatMap(List::stream) .forEach(System.out::println);- 一对多映射
- 常用于"嵌套集合"
-
distinct------去重
javastream.distinct();- 内部使用
HashSet - 依赖
equals/hashCode - 有状态操作
- 内部使用
-
sorted------排序
javastream.sorted(); Stream.sorted(Comparator.comparingInt(User::getAge));- 需要缓存所有元素
- 有状态操作
- 会破坏元素流动
-
limit/skip------截断
javastream.limit(5); stream.skip(3);limit是短路操作- 达到数量后立即停止流水线
3、终止操作(触发执行)
-
forEach------消费
javastream.forEach(System.out::println);- 使用
Consumer<T> - 并行流中不保证顺序
- 不建议在forEach中做复杂副作用
- 使用
-
collect------收集(常用)
javaList<Integer> list = stream.collect(Collectors.toList());本质:
javasupplier // 创建容器 accumulator // 添加元素 combiner // 并行合并 -
reduce------规约
javaint sum = stream.reduce(0, Integer::sum);- 把多个元素合成一个
- 要求:
- 无副作用
- 满足结合律
-
count/findFirst/anyMatch
javastream.count(); stream.findFirst(); stream.anyMatch(x -> x > 10)findFirst/anyMatch支持短路- 找到结果立即停止
13 ForkJoin
一、为什么要有ForkJoin体系
-
传统线程池的问题:不擅长递归任务
在普通线程池(ThreadPoolExecutor)中:
-
任务是整体提交
-
一个任务通常不能再拆分
-
CPU密集型任务容易:
- 线程忙
- CPU利用率不均
- 某些线程闲着,某些线程累死
-
-
ForkJoin要解决什么问题
让一个大任务可以不断拆成小任务,并行执行,再把结果合并
典型场景:
- 大数组求和
- 排序
- 树/图遍历
- Stream并行计算
二、什么是ForkJoin体系
ForkJoin是Java提供的一套用于"分治并行计算"的框架
核心思想来自算法中的:Divide&Conquer(分而治之)
ForkJoin的核心组件
| 组件 | 作用 |
|---|---|
| ForkJoinPool | 线程池 |
| ForkJoinWorkerThread | 工作线程 |
| ForkJoinTask | 可拆分任务 |
| RecursiveTask | 有返回值任务 |
| RecursiveAction | 无返回值任务 |
三、ForkJoin的基本工作流程
perl
一个大任务
↓ fork
拆成多个小任务
↓ fork
子任务并行执行
↓ join
合并子任务结果
四、ForkJoin的核心思想:分治模型
典型伪代码:
java
if(任务足够小) {
直接计算;
}else {
拆分成子任务;
fork子任务;
join子任务结果;
}
- 递归 + 并行
五、ForkJoin的核心类
1、ForkJoinPool------专门为"分治"设计的线程池
特点
- 默认线程数 ≈ CPU核心数
- 专门为CPU密集型任务设计
- 内部不使用普通阻塞队列
java
ForkJoinPool pool = new ForkJoinPool();
与ThreadPoolExecutor的关键区别
| 对比 | ThreadPoolExecutor | ForkJoinPool |
|---|---|---|
| 任务模型 | 独立任务 | 可拆分任务 |
| 队列 | BlockingQueue | 双端队列 |
| 目标 | IO / 通用 | CPU密集 |
| 核心机制 | 抢占 | 工作窃取 |
2、ForkJoinTask------抽象任务
两个常用子类
-
RecursiveTask<T>(有返回值)。
javaclass SumTask extends RecuisiveTask<Integer> {} -
RecursiveAction<T>(无返回值)
javaclass PrintTask extends RecersiveAction {}
六、工作窃取
忙的线程不帮闲的,闲的线程主动偷活干
如何做到工作窃取
-
每个
ForkJoinWorkerThread,都有一个自己的双端队列(Deque) -
规则:
- 自己提交的任务:LIFO(栈)
- 窃取别人的任务:FIFO(队列)
tex线程 A: [ T1, T2, T3 ] 线程 B: [ ] 线程 B 偷走 T1这样做的好处:
- 减少锁竞争
- 提高CPU利用率
- 局部性更好(缓存友好)
为什么不用BlockingQueue
- 锁竞争太重
- 不适合大量小任务
- 工作窃取需要双端结构
七、ForkJoin示例
fork()是"把活甩给别人"
compute()是"自己动手干活"
join()是"等别人把活干完,拿结果"
java
public class ForkJoinTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Long startTime = System.currentTimeMillis();
long sum = test01(); // Time = 413ms
// Long sum = test02(); // Time = 229ms
// Long sum = test03(); // Time = 84ms
Long endTime = System.currentTimeMillis();
System.out.println("sum = " + sum + " Time = " + (endTime - startTime) + "ms");
}
public static long test01() { // 直接计算
long sum = 0L;
for (long i = 1L; i <= 10_0000_0000; i++) {
sum += i;
}
return sum;
}
public static Long test02() throws ExecutionException, InterruptedException { // ForkJoin
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> forkJoinTask = new SumTask(1L, 10_0000_0000L, 10000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinTask);
return submit.get();
}
public static Long test03() { // Stream 并行计算
return LongStream.rangeClosed(1L, 10_0000_0000L).parallel().reduce(0, Long::sum);
}
}
class SumTask extends RecursiveTask<Long> {
final long start;
final long end;
final long limit;
SumTask(long start, long end, long limit) {
this.start = start;
this.end = end;
this.limit = limit;
}
@Override
protected Long compute() {
if (end - start <= limit) {
long sum = 0;
for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
}else {
SumTask sumTaskLeft = new SumTask(start, limit, start + (limit - start) / 2);
SumTask sumTaskRight = new SumTask(limit + 1, end, limit + (end - limit - 1) / 2);
sumTaskLeft.fork();
Long rightResult = sumTaskRight.compute();
Long leftResult = sumTaskLeft.join();
return rightResult + leftResult;
}
}
}
八、fork() / join()
fork()
- 把任务压入 当前线程的工作队列
- 异步执行
join()
- 等待子任务完成
- 如果子任务没执行
- 当前线程可能自己执行
- 或等待别人执行
总结
Fork / Join 是 Java 提供的一种基于分治思想的并行计算框架,通过将大任务递归拆分为多个子任务并行执行,并结合工作窃取算法提高线程利用率,特别适合 CPU 密集型场景,也是并行 Stream 的底层执行机制。
14 异步回调
一、为什么需要Future
同步调用的问题
java
int result = slowMethod(); // 阻塞
问题在于:
- 当前线程必须等待结果
- CPU / 线程资源被浪费
- 不适合耗时操作(IO / 网络 / RPC)
Future的目标:把"任务执行"和"结果获取"分离,让任务可以异步执行。
二、Future接口详解
1、Future是什么
java
public interface Future<V> {
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit);
boolean cancel(boolean mayInterruptIfRunning);
boolean isDone();
boolean isCancelled();
}
Future表示"一个未来才会产生的结果"
2、Future的典型使用方式
java
ExecutorService pool = Executors.newFixedThreadPool(1);
Future<Integer> future = pool.submit(()->{
TimeUnit.SECONDS.sleep(1);
return 100;
});
System.out.println("主线程继续执行");
Integer result = future.get(); // 阻塞
System.out.println(result);
执行过程:
- 任务提交后立即返回Future
- 任务在其他线程执行
- 调用
get()时阻塞等待结果
3、Future的核心作用总结
| 能力 | 说明 |
|---|---|
| 异步执行 | 任务在其他线程执行 |
| 结果占位 | Future是结果的"句柄" |
| 阻塞获取 | 通过 get() 拿结果 |
| 取消任务 | cancel() |
Future = 异步执行 + 阻塞式结果获取
三、Future的不足(重点)
Future虽然解决了"异步执行",但存在明显短板。
-
只能阻塞式获取结果
javafuture.get(); '' 必须等- 要么阻塞
- 要么不停
isDone()轮询
-
不支持异步回调
做不到:
java任务完成后 -> 自动执行某段逻辑只能:
javaget() -> 再手动处理 -
不能组合多个异步任务
场景:
- 任务A完成 → 再执行任务B
- 任务A、B都完成 → 合并结果
Future 写起来会:
- 回调地狱
- 阻塞嵌套
- 代码可读性极差
-
异常处理不友好
javatry{ future.get(); } catch(ExecutionException e) { // 包了一层异常 }
Future 只能"拿结果",却不能"编排异步流程"。
四、FutureTask:过渡方案(了解)
java
public class Future<V> implements RunnableFuture<V>
特点:
- 同时是
Runnable+Future Callable只会执行一次(CAS保证)- 结果会被缓存,多线程共享
用法:
java
FutureTask<Integer> task = new FutureTask<>(()->100);
new Thread(task).start();
Integer result = task.get;
但是仍然不支持回调和链式编排
五、CompletableFuture(重点)
1、CompletableFuture是什么
CompletableFuture 是对 Future 的增强版,支持异步回调、任务组合和函数式编排。
CompletableFuture = Future + 回调 + 编排 + 函数式
2、CompletableFuture解决了什么
| Future的问题 | CompletableFuture的解决方案 |
|---|---|
| get 阻塞 | thenXXX 异步回调 |
| 无法组合 | thenCompose / thenCombine |
| 异常难处理 | exceptionally / handle |
| 手动管理流程 | 链式声明式编排 |
六、 CompletableFuture 的基本使用方法
创建异步任务
-
无返回值
javaCompletableFuture.runAsync(()->{ System.out.println("异步任务") }) -
有返回值
javaCompletableFuture<Integer> cf = CompletableFuture.supplyAsync(()->100);
默认线程池:ForkJoinPool.commonPool()
也可以指定线程池:
java
CompletableFuture.supplyAsync(()->100, executor);
七、什么是"异步回调"(核心)
不是我去等结果,而是结果出来后自动通知我
三类回调方法(重要)
-
thenApply------结果转换(Function)
javaCompletableFuture<Integer> cf = CompletableFuture.supplyAsync(()->10) .thenApply(x->x * 2);- 有输入
- 有输出
- 类似Stream的
map
-
thenAccept------消费结果(Consumer)
javaCompletableFuture.supplyAsync(()->10) .thenAccept(System.out::println) .thenAccept(x->System.out.println(x));- 有输入
- 无返回值
- 常用于打印、保存、发送
-
thenRun------不关心结果
javaCompletableFuture.supplyAsync(()->100) .thenRun(()->System.out.println("完成"));
八、异步任务的组合能力(CompletableFuture最大优势)
-
thenCompose------串行依赖(扁平化)
javaCompletableFuture<Integer> cf = CompletableFuture.supplyAsync(()->10) .thenCompose(x-> CompletableFuture.supplyAsync(()->x * 2) );- A完成后,在启动B
-
thenCombine------并行合并
javaCompletableFuture<Integer> cf1 = CompletableFuture.supply(()->10); CompletableFuture<Integer> cf2 = CompletableFuture.supply(()->20); CompletableFuture<Integer> result = cf1.thenCombine(f2, Integer::sum);- 两个任务并行执行
- 都完成后合并结果
九、异常处理(CompletableFuture的强项)
-
exceptionally------兜底异常
javaCompletableFuture.supply(()->1 / 0) .exceptionally(ex->-1); -
handle------成功 / 失败统一处理
javaCompletableFuture.supplyAsync(()->10) .handle((rex, ex)->{ if(ex != nell) return -1; return res*2; });
十、CompletableFuture 项目案例
java
public class CompletableFutureTest {
// 真实项目中使用自定义线程池 避免吧commonPool用满
private static final ExecutorService IO_POOL = Executors.newFixedThreadPool(
6,
(r)->{
Thread t = new Thread(r);
t.setName("io_pool-" + t.getId());
t.setDaemon(false);
return t;
});
private static final Random RND = new Random();
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFutureTest demo = new CompletableFutureTest();
long start = System.currentTimeMillis();
// 1. 并发查询三个"互不依赖"的信息
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(demo::queryUser, IO_POOL);
//System.out.println(userFuture.get());
CompletableFuture<Integer> scoreFuture = CompletableFuture.supplyAsync(demo::queryScore, IO_POOL);
//System.out.println(scoreFuture.get());
CompletableFuture<Integer> orderCountFuture = CompletableFuture.supplyAsync(demo::queryOrderCount, IO_POOL);
// 2. thenApply 对 积分结果 做转换 -> 生成等级(VIP / NORMAL)
CompletableFuture<String> levelFuture = scoreFuture.thenApply((score)-> score > 80 ? "VIP" : "NORMAL");
// 3. thenCombine 并行合并 user + level -> 生成 UserDTO
CompletableFuture<UserDTO> userDTOFuture =
userFuture.thenCombine(levelFuture, (user, level)->new UserDTO(user.id, user.name, level));
// 4. 合并订单数 UserDTO + orderCount
CompletableFuture<UserDTO> fullDTOfuture =
userDTOFuture.thenCombine(orderCountFuture, (dto, orderCount)->{
dto.orderCount = orderCount;
return dto;
});
// 5. 串行依赖(必须先拿到优惠券 userID 才能查优惠券)
CompletableFuture<List<Coupon>> couponFuture =
userFuture.thenCompose((user)->
CompletableFuture.supplyAsync(()->demo.queryCoupons(user.id), IO_POOL));
// 6. 再把优惠券结果合并进 DTO(dto + coupons)
CompletableFuture<UserDTO> finalPageFuture =
userDTOFuture.thenCombine(couponFuture, (dto, couponsList)->{
dto.coupons = couponsList;
return dto;
});
// 7. 异常处理示例A exceptionally(只在异常时兜底)
CompletableFuture<Integer> safeScoreFuture =
CompletableFuture.supplyAsync(demo::queryScoreWithPossibleFailure, IO_POOL)
.exceptionally((ex)->{
System.out.println("积分服务异常:" + ex.getMessage());
return 0;
});
// 8. 异常处理实例B handle(成功 / 失败都统一处理)
CompletableFuture<String> scoreMsgFuture =
CompletableFuture.supplyAsync(demo::queryScoreWithPossibleFailure, IO_POOL)
.handle((score, ex)->{
if(ex != null) return "积分服务暂不可用";
return "积分:" + score;
});
// 9. allOf:等待一组任务都完成(常用于打点 / 统计 / 日志)
CompletableFuture<Void> allMainTaskDone =
CompletableFuture.allOf(userFuture, scoreFuture, orderCountFuture, couponFuture);
allMainTaskDone.thenRun(()-> System.out.println("[打点] user / score / order / coupon 四个任务全部完成"));
// 10. thenAccept:异步回调"最终消费结果"(模拟渲染页面 / 返回接口)
finalPageFuture.thenAccept((dto)->{
long cost = System.currentTimeMillis() - start;
System.out.println("\n=== 页面渲染 DTO(回调触发)===");
System.out.println(dto);
System.out.println("耗时约:" + cost + "ms");
});
// 11. 也可以 join 获取最终结果(会阻塞当前线程直到完成)
// 注意:join抛 RuntimeException;get 抛受检异常
UserDTO dto = finalPageFuture.join();
System.out.println("\n=== 主线程 join 得到最终 DTO ===");
System.out.println(dto);
// 12. 看看异常处理示例的结果
System.out.println("\n=== 异常处理示例输出 ===");
System.out.println("safeScoreFuture = " + safeScoreFuture.join());
System.out.println("scoreMsgFuture = " + scoreMsgFuture.join());
// 关闭线程池(demo 必做,真实服务一般在shutdown hook中做)
IO_POOL.shutdown();
}
/*--------------------------------------------------------------------------
模拟:数据结构
* (User / Coupon / DTO)
* -------------------------------------------------------------------------*/
static class User {
final int id;
final String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
}
static class Coupon { // 优惠券
final String code;
public Coupon(String code) {
this.code = code;
}
@Override
public String toString() {
return code;
}
}
// "UserDTO" 通常指 "User Data Transfer Object",即用户数据传输对象。
// 在软件开发中,它是一种设计模式,用于在不同的层(比如表现层和业务逻辑层之间)传递数据。
static class UserDTO {
final int id;
final String name;
final String level;
int orderCount;
List<Coupon> coupons;
public UserDTO(int id, String name, String level) {
this.id = id;
this.name = name;
this.level = level;
}
@Override
public String toString() {
return "UserDTO{" +
"id=" + id +
", name='" + name + '\'' +
", level='" + level + '\'' +
", orderCount=" + orderCount +
", coupons=" + coupons +
'}';
}
}
/*--------------------------------------------------------------------------
* 模拟:耗时查询方法(业务方法)
* ------------------------------------------------------------------------*/
User queryUser() {
sleepMs(800);
System.out.println(threadTag() + "queryUser done");
return new User(1, "张三");
}
// 模拟查询积分(例如远程 RPC)
// "远程 RPC" 指远程过程调用(Remote Procedure Call)。
// 它是一种通过网络让程序调用另一个地址空间(通常是不同主机)里的过程或函数,无需开发者显式编写网络通信相关代码的技术。
Integer queryScore() {
sleepMs(1200);
System.out.println(threadTag() + "queryScore done");
return 88;
}
// 模拟查询订单数
Integer queryOrderCount() {
sleepMs(900);
System.out.println(threadTag() + "queryOrderCount done");
return 5;
}
// 模拟:先知道userID 才能查询优惠券(串行依赖场景)
List<Coupon> queryCoupons(int userID) {
sleepMs(700);
System.out.println(threadTag() + "queryCoupons(userID= " + userID + ") done");
// 模拟优惠券
return IntStream.rangeClosed(1, 3)
.mapToObj((i)-> new Coupon("CP-" + userID + "-" + i))
.collect(Collectors.toList());
}
// 模拟:积分服务有一定概率失败,用于演示 execptionally / handle
public Integer queryScoreWithPossibleFailure() {
sleepMs(600);
// 30%概率抛异常
if(RND.nextInt(10) < 3) {
throw new RuntimeException("score service timeout");
}
System.out.println(threadTag() + "queryScoreWithPossibleFailure done");
return 90;
}
/*--------------------------------------------------------------------------
* 工具方法
* ------------------------------------------------------------------------*/
static void sleepMs(long ms) {
try {
TimeUnit.MILLISECONDS.sleep(ms);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 真实业务:通常应该恢复中断状态
}
}
static String threadTag() {
return "[" + Thread.currentThread().getName() + "] ";
}
}
Future vs CompletableFuture 总对比
| 维度 | Future | CompletableFuture |
|---|---|---|
| 异步执行 | ✅ | ✅ |
| 获取方式 | 阻塞get | 非阻塞回调 |
| 支持回调 | ❌ | ✅ |
| 支持组合 | ❌ | ✅ |
| 异常处理 | 弱 | 强 |
| 编程模型 | 命令式 | 声明式 |
| 底层 | 线程池 | ForkJoinPool |
如何选择Future / CompletableFuture
- 用Future
- 代码简单
- 只关心最终结果
- 老系统 / 兼容性需求
- 用CompletableFuture
- 多异步任务编排
- IO / RPC / 微服务
- 追求非阻塞、高并发
总结
Future 接口用于表示一个异步计算的结果,但只能通过阻塞或轮询的方式获取结果,缺乏回调和任务组合能力。CompletableFuture 在 Future 的基础上引入了异步回调、任务编排和函数式编程模型,使异步流程可以以非阻塞、链式的方式组织,极大提升了并发程序的可读性和扩展性。