面试题答案
一键面试Java CompletableFuture的线程使用模型
- 默认执行线程
- 无显式线程指定:
- 如果
CompletableFuture
任务没有显式指定线程执行,对于计算密集型任务(如supplyAsync
的计算逻辑),默认使用ForkJoinPool.commonPool()
线程池中的线程。ForkJoinPool.commonPool()
是一个共享的线程池,其线程数量默认是Runtime.getRuntime().availableProcessors() - 1
(至少为1)。 - 例如:
- 如果
- 无显式线程指定:
CompletableFuture.supplyAsync(() -> {
// 计算逻辑
return "result";
});
这里的计算逻辑就会在ForkJoinPool.commonPool()
线程池的线程中执行。
- 基于已有CompletableFuture的后续操作:对于依赖于已有
CompletableFuture
结果的后续操作(如thenApply
、thenAccept
等),如果前序CompletableFuture
已经完成,这些后续操作会在当前调用线程执行。如果前序CompletableFuture
未完成,后续操作会在完成前序CompletableFuture
的线程中执行。例如:
CompletableFuture.supplyAsync(() -> "hello")
.thenApply(s -> s + " world");
如果supplyAsync
任务很快完成,thenApply
操作会在当前调用线程执行;如果supplyAsync
任务未完成,thenApply
操作会在完成supplyAsync
任务的线程中执行。
2. 指定执行线程
- 可以通过
CompletableFuture.supplyAsync(Supplier<U> supplier, Executor executor)
或CompletableFuture.runAsync(Runnable runnable, Executor executor)
方法指定线程执行任务。例如:
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletableFuture.supplyAsync(() -> {
// 计算逻辑
return "result";
}, executor);
这里的任务就会在executor
线程池的线程中执行。
大量CompletableFuture任务并发执行时线程资源优化
- 合理配置线程池
- 使用自定义线程池:避免过度使用
ForkJoinPool.commonPool()
,因为共享线程池可能会被其他Fork/Join
任务或CompletableFuture
任务竞争。创建自定义线程池时,需要根据任务的类型(I/O密集型或计算密集型)来合理设置线程数量。- 计算密集型任务:线程数量可以设置为
Runtime.getRuntime().availableProcessors()
,以充分利用CPU核心。例如:
- 计算密集型任务:线程数量可以设置为
- 使用自定义线程池:避免过度使用
ExecutorService cpuBoundExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
- **I/O密集型任务**:由于I/O操作会使线程阻塞,线程数量可以设置得比CPU核心数多,一般可以设置为`2 * Runtime.getRuntime().availableProcessors()`甚至更多,以充分利用等待I/O的时间。例如:
ExecutorService ioBoundExecutor = Executors.newFixedThreadPool(2 * Runtime.getRuntime().availableProcessors());
- 使用线程池参数优化:对于
ThreadPoolExecutor
,可以调整corePoolSize
、maximumPoolSize
、keepAliveTime
等参数。例如,对于I/O密集型任务,可以适当增大corePoolSize
,使线程池一开始就有足够的线程处理任务,减少线程创建和销毁的开销。对于keepAliveTime
,如果任务执行时间较短且并发量波动较大,可以适当设置较短的keepAliveTime
,以回收闲置线程。
- 任务分解与合并
- 分解大任务:如果有大的计算任务,可以将其分解为多个小任务,利用
CompletableFuture
的并行特性,通过CompletableFuture.allOf
等方法并行执行这些小任务,然后合并结果。例如,假设要对一个大数组进行计算,可以将数组分成多个部分,每个部分由一个CompletableFuture
任务处理,最后合并结果:
- 分解大任务:如果有大的计算任务,可以将其分解为多个小任务,利用
int[] largeArray = new int[10000];
// 初始化数组
ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
CompletableFuture[] futures = new CompletableFuture[4];
int partSize = largeArray.length / 4;
for (int i = 0; i < 4; i++) {
int start = i * partSize;
int end = (i == 3)? largeArray.length : (i + 1) * partSize;
futures[i] = CompletableFuture.supplyAsync(() -> {
int sum = 0;
for (int j = start; j < end; j++) {
sum += largeArray[j];
}
return sum;
}, executor);
}
CompletableFuture<Void> allFutures = CompletableFuture.allOf(futures);
CompletableFuture<Integer> totalSumFuture = allFutures.thenApply(v -> {
int totalSum = 0;
for (CompletableFuture<Integer> future : futures) {
try {
totalSum += future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
return totalSum;
});
- 合并小任务:如果有大量非常小的任务,可以将它们合并成较大的任务块,减少线程切换的开销。例如,假设要对多个小文件进行读取操作,可以将几个小文件的读取任务合并为一个任务。
- 优化任务依赖关系
- 减少不必要的依赖:检查
CompletableFuture
任务之间的依赖关系,确保只有真正需要顺序执行的任务存在依赖。避免创建过多不必要的链式依赖,因为这可能会限制并行性。例如,如果有一系列任务A、B、C、D,其中A和B相互独立,C依赖于A和B的结果,D依赖于C的结果,可以先并行执行A和B,然后再执行C和D,而不是依次顺序执行。 - 使用异步化依赖处理:对于有依赖关系的任务,可以尽量异步化依赖处理。例如,如果任务B依赖于任务A的结果,可以在任务A完成后,使用
thenApplyAsync
等方法异步执行任务B,而不是在任务A的线程中同步执行任务B,这样可以提高线程的利用率。
- 减少不必要的依赖:检查