优雅使用 CompletableFuture 实现并行计算
关于性能优化,我们通常划分为五大优化方向,有预计算、并行计算、异步计算、存储系统优化、其他算法优化。
在这里,我主要讲一下最常用的并行计算,其体现的思想是 “人多力量大,众人拾柴火焰高”,旨在通过将任务拆解后,以多路并行的方式,将任务执行的总时长进行缩短,以达到提升性能的目的。
在 java 中,要实现并行执行任务的话,离不开多线程并行利器 CompletableFuture 的 allOf() 方法,下面主要来介绍CompletableFuture
1. 什么是 CompletableFuture
CompletableFuture 是 Java 提供的一个 Future 的增强版本,具有以下特点:
- 非阻塞式编程:通过回调函数处理任务结果,而不是阻塞等待。
- 丰富的组合能力:支持任务的串行和并行组合。
- 异常处理:方便地处理异步任务中的异常。
- 支持自定义线程池:通过显式线程池提高性能或定制化任务执行环境。
2. CompletableFuture 的基本使用
2.1 创建 CompletableFuture
可以通过以下方式创建:
CompletableFuture<String> future = new CompletableFuture<>();
但通常更常用的是工厂方法:
- supplyAsync: 用于有返回值的异步任务。
- runAsync: 用于无返回值的异步任务。
示例:
CompletableFuture<String> supplyFuture = CompletableFuture.supplyAsync(() -> {
return "Hello, CompletableFuture!";
});
CompletableFuture<Void> runFuture = CompletableFuture.runAsync(() -> {
System.out.println("Executing async task...");
});
2.2 任务的串行化
可以通过 thenApply、thenAccept 等方法处理异步任务的结果。
示例:
CompletableFuture.supplyAsync(() -> "Task 1")
.thenApply(result -> result + " -> Task 2")
.thenAccept(System.out::println);
输出:
Task 1 -> Task 2
2.3 任务的并行组合
thenCombine 和 allOf 可以用来组合多个任务。
示例:
package com.example.demo;
import java.util.concurrent.*;
public class Order {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Order order = new Order();
order.checkOrder();
}
public boolean checkOrder() {
if (!basicCheck()) {
return false;
}
CompletableFuture<Boolean> checkRiskControl = CompletableFuture.supplyAsync(() -> {
return true;
});
CompletableFuture<Boolean> checkCoupon = CompletableFuture.supplyAsync(() -> {
return true;
});
CompletableFuture<Boolean> checkPoint = CompletableFuture.supplyAsync(() -> {
return true;
});
CompletableFuture<Boolean> checkGoods = CompletableFuture.supplyAsync(() -> {
return true;
});
CompletableFuture<Boolean> checkInventory = CompletableFuture.supplyAsync(() -> {
return true;
});
CompletableFuture<Boolean> result = CompletableFuture.allOf(checkRiskControl, checkCoupon, checkPoint, checkGoods, checkInventory)
.thenApply(res -> {
return checkRiskControl.join() && checkCoupon.join() && checkPoint.join() && checkGoods.join() && checkInventory.join();
});
System.out.println("订单完成前置校验结果为" + result.join());
return true;
}
public boolean basicCheck() {
return true;
}
}
日志打印如下:
订单完成前置校验结果为true
2.4 异常处理
通过 exceptionally 或 handle 处理异常:
CompletableFuture.supplyAsync(() -> {
if (true) throw new RuntimeException("Something went wrong");
return "Success";
}).exceptionally(ex -> {
System.out.println("Exception: " + ex.getMessage());
return "Fallback";
}).thenAccept(System.out::println);
输出:
Exception: Something went wrong
Fallback
3. 自定义线程池
默认情况下,CompletableFuture 使用 ForkJoinPool.commonPool() 执行任务。但在一些场景中,例如需要更好的性能控制或隔离任务,可以使用自定义线程池。
3.1 创建线程池
根据任务类型的不同,可以选择合适的线程池配置。
CPU 密集型任务
对于 CPU 密集型任务,其线程池配置应尽量避免线程上下文切换,从而提升 CPU 的利用率。一般情况下,线程池的核心线程数和最大线程数建议设置为等于或略小于 CPU 核心数(Runtime.getRuntime().availableProcessors()),这样线程数刚好能充分利用 CPU,而不会因为过多线程竞争 CPU 而导致频繁上下文切换。因此,线程数通常设置为 N 或 N+1,其中 N 是可用处理器核心数:
private final ExecutorService executorService = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(), // 核心线程数
Runtime.getRuntime().availableProcessors(), // 最大线程数
60L, // 空闲线程最大存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(), // 阻塞队列,选择无界队列防止任务丢失
Executors.defaultThreadFactory(), // 默认线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略:抛出异常
);
IO 密集型任务
对于 IO 密集型任务,线程通常会因为等待 IO 操作(如网络请求、磁盘读写等)而处于阻塞状态,CPU 并不一直被占用。因此,IO 型任务的线程池通常需要更多线程以充分利用等待时间,从而提升吞吐量。因此,线程数通常设置为 2N 到 4N,以充分利用线程的等待时间:
private final ExecutorService ioBoundExecutor = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors() << 1,
Runtime.getRuntime().availableProcessors() << 2,
60,
TimeUnit.SECONDS,
new SynchronousQueue<>(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
3.2 使用线程池
将线程池传递给 supplyAsync 或 runAsync 方法:
CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + " is executing the task");
return "Result from custom thread pool";
}, ioBoundExecutor).thenAccept(System.out::println);
输出示例:
Custom-Executor-Thread is executing the task
Result from custom thread pool
3.3 关闭线程池
不要忘记在任务完成后关闭线程池:
ioBoundExecutor.shutdown();
cpuBoundExecutor.shutdown();
4. 串行与并行的对比
以下示例使用 CompletableFuture 实现并行任务和串行任务的对比,来体现并行计算的优势。
import java.util.concurrent.*;
public class CompletableFutureExample {
public static void main(String[] args) throws Exception {
// 自定义线程池
ExecutorService customThreadPool = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(), // 核心线程数
Runtime.getRuntime().availableProcessors(), // 最大线程数
60L, // 空闲线程最大存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(), // 阻塞队列,选择无界队列防止任务丢失
Executors.defaultThreadFactory(), // 默认线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略:抛出异常
);
// 模拟任务
Callable<Integer> task1 = () -> {
System.out.println(Thread.currentThread().getName() + " is executing task1");
Thread.sleep(2000);
return 1;
};
Callable<Integer> task2 = () -> {
System.out.println(Thread.currentThread().getName() + " is executing task2");
Thread.sleep(3000);
return 2;
};
Callable<Integer> task3 = () -> {
System.out.println(Thread.currentThread().getName() + " is executing task3");
Thread.sleep(1000);
return 3;
};
// 串行执行
long startSerial = System.currentTimeMillis();
int result1 = task1.call();
int result2 = task2.call();
int result3 = task3.call();
long endSerial = System.currentTimeMillis();
System.out.println("Serial Execution Result: " + (result1 + result2 + result3));
System.out.println("Serial Execution Time: " + (endSerial - startSerial) + "ms\n");
// 并行执行
long startParallel = System.currentTimeMillis();
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
try {
return task1.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
}, customThreadPool);
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
try {
return task2.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
}, customThreadPool);
CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(() -> {
try {
return task3.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
}, customThreadPool);
CompletableFuture<Void> allOf = CompletableFuture.allOf(future1, future2, future3);
// 等待所有任务完成
allOf.join();
int parallelResult = future1.get() + future2.get() + future3.get();
long endParallel = System.currentTimeMillis();
System.out.println("Parallel Execution Result: " + parallelResult);
System.out.println("Parallel Execution Time: " + (endParallel - startParallel) + "ms\n");
// 关闭线程池
customThreadPool.shutdown();
}
}
运行结果:
main is executing task1
main is executing task2
main is executing task3
Serial Execution Result: 6
Serial Execution Time: 6009ms
pool-1-thread-1 is executing task1
pool-1-thread-2 is executing task2
pool-1-thread-3 is executing task3
Parallel Execution Result: 6
Parallel Execution Time: 3012ms
运行后可以观察到,串行执行耗时较长,而并行执行通过并发显著缩短了时间。
5. 总结
通过 CompletableFuture 和自定义线程池,可以灵活优化并行任务的执行性能。
- 并行任务通过多线程同时执行,总耗时取决于最慢任务,显著提升了性能。
- 根据任务类型来选择合适的线程池配置,能够有效避免 cpu 资源浪费,并提高程序的整体吞吐量。

2250

被折叠的 条评论
为什么被折叠?



