面试题答案
一键面试可能存在的问题
- 性能开销:
handle
方法会引入额外的函数调用和上下文切换开销。每次调用handle
都会创建一个新的CompletionStage
,这在高并发场景下会消耗大量的系统资源。 - 线程阻塞:如果
handle
方法中的逻辑执行时间过长,可能会导致线程阻塞,影响整体的并发性能。 - 线程模型问题:默认情况下,
CompletableFuture
使用ForkJoinPool.commonPool()
来执行异步任务,这个线程池是所有CompletableFuture
共享的。在高并发场景下,可能会出现线程饥饿问题,即某些任务长时间得不到执行。
优化策略
- 自定义线程池:通过创建自定义的线程池,可以根据业务需求调整线程池的大小、队列容量等参数,避免线程饥饿问题。
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class CompletableFutureOptimization {
private static final AtomicInteger COUNTER = new AtomicInteger(0);
private static final ThreadFactory THREAD_FACTORY = r -> {
Thread thread = new Thread(r);
thread.setName("CustomThread-" + COUNTER.incrementAndGet());
return thread;
};
private static final ExecutorService EXECUTOR = new ThreadPoolExecutor(
10,
20,
10L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
THREAD_FACTORY);
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> "Hello", EXECUTOR)
.handle((result, ex) -> {
if (ex != null) {
ex.printStackTrace();
return "Error";
}
return result + " World";
})
.thenAccept(System.out::println);
EXECUTOR.shutdown();
}
}
- 减少
handle
方法的嵌套:尽量将多个handle
方法合并成一个,减少不必要的函数调用和上下文切换。
CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(s -> s + " World")
.handle((result, ex) -> {
if (ex != null) {
ex.printStackTrace();
return "Error";
}
return result;
})
.thenAccept(System.out::println);
优化后:
CompletableFuture.supplyAsync(() -> "Hello")
.handle((s, ex) -> {
if (ex != null) {
ex.printStackTrace();
return "Error";
}
return s + " World";
})
.thenAccept(System.out::println);
- 异步化
handle
方法中的逻辑:如果handle
方法中的逻辑执行时间较长,可以将其异步化,避免阻塞线程。
CompletableFuture.supplyAsync(() -> "Hello")
.handleAsync((result, ex) -> {
if (ex != null) {
ex.printStackTrace();
return "Error";
}
// 模拟长时间运行的任务
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return result + " World";
}, EXECUTOR)
.thenAccept(System.out::println);
handle
方法线程调度和管理
- 默认线程池:如果没有指定线程池,
handle
方法会使用ForkJoinPool.commonPool()
来执行异步任务。这个线程池是所有CompletableFuture
共享的,线程数量默认是Runtime.getRuntime().availableProcessors() - 1
。 - 自定义线程池:通过在
supplyAsync
、handleAsync
等方法中传入自定义的Executor
,可以使用自定义的线程池来执行任务。这样可以根据业务需求调整线程池的参数,提升整体性能。
调整线程池配置提升性能
- 线程池大小:根据任务的类型(CPU密集型或I/O密集型)调整线程池的核心线程数和最大线程数。对于CPU密集型任务,线程池大小一般设置为
Runtime.getRuntime().availableProcessors()
;对于I/O密集型任务,可以适当增大线程池大小。 - 队列容量:根据任务的数量和处理速度调整队列容量。如果队列容量过小,可能会导致任务被拒绝;如果队列容量过大,可能会导致任务在队列中等待时间过长。
- 线程存活时间:合理设置线程的存活时间,避免线程频繁创建和销毁带来的开销。如果任务执行频率较高,可以适当延长线程的存活时间。