1. Java 中的线程池有哪些核心参数?你在项目中是怎么配置的?
考察点:ThreadPoolExecutor 参数
参考答案:
ThreadPoolExecutor 有 7 个核心参数:
new ThreadPoolExecutor(
corePoolSize, // 核心线程数
maximumPoolSize, // 最大线程数
keepAliveTime, // 空闲线程存活时间
timeUnit, // 时间单位
workQueue, // 工作队列
threadFactory, // 线程工厂
rejectedHandler // 拒绝策略
);
在项目里,我们针对不同场景配置了不同的线程池,比如说工作流节点执行线程池的配置思路是:核心线程数根据 CPU 核数和任务类型定,IO 密集型可以多一些;最大线程数根据系统资源和并发量定;队列用 SynchronousQueue 是因为我们希望任务尽快执行,不要排队;拒绝策略我们选择了 CallerRunsPolicy 可以起到"限流"作用
// 工作流节点执行线程池
ThreadPoolExecutor nodeExecutor = new ThreadPoolExecutor(
10, // 核心 10 个线程
50, // 最大 50 个线程
60L, TimeUnit.SECONDS, // 空闲 60 秒回收
new SynchronousQueue<>(), // 不排队,直接创建线程
new NamedThreadFactory("node-executor"),
new CallerRunsPolicy()
);
参考答案版本 2:
ThreadPoolExecutor 有 7 个核心参数:
corePoolSize:核心线程数,线程池会始终保持这么多线程存活,即使它们是空闲的。
maximumPoolSize:最大线程数,当任务太多、核心线程忙不过来时,可以创建额外的线程,但总数不超过这个值。
keepAliveTime + unit:非核心线程的空闲存活时间。额外创建的线程如果空闲超过这个时间,就会被回收。
workQueue:任务队列,核心线程忙的时候,新任务先放队列里排队。队列满了才会创建额外线程。
threadFactory:线程工厂,用来创建线程,可以定制线程名称、优先级等。
handler:拒绝策略,当队列满了、线程也满了,新任务怎么处理。常见的有抛异常、丢弃、调用者执行等。
任务提交的流程是这样的:任务进来 → 核心线程有空闲吗?有就执行,没有就放队列 → 队列满了吗?没满就排队,满了就创建新线程 → 线程数到上限了吗?没到就创建,到了就执行拒绝策略。
在 PaiFlow 中,我们有几种不同的线程池,针对不同场景配置不同。
第一种是 SSE 发送线程池,这个场景是 IO 密集型,线程大部分时间在等待网络 IO,所以核心线程数我们设置了 20,是因为日常大概有十几个并发 SSE 连接。最大线程设 100 是为了应对突发流量。拒绝策略用 CallerRunsPolicy,这样即使线程池满了,任务也不会丢,只是会让调用者线程来执行,起到降速的作用。
@Bean("sseSendExecutor")
public ThreadPoolExecutor sseSendExecutor() {
return new ThreadPoolExecutor(
20, // 核心线程:支撑日常并发
100, // 最大线程:应对突发流量
60, TimeUnit.SECONDS, // 空闲 60 秒回收
new LinkedBlockingQueue<>(500), // 队列容量 500
new ThreadFactoryBuilder().setNameFormat("sse-send-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 满了就让调用者自己执行
);
}
第二种是工作流执行线程池,用于节点的并行执行。这个场景混合了 CPU 计算和 IO 等待,这里用 AbortPolicy 是因为工作流执行很重要,如果线程池满了说明系统已经过载,不如快速失败让上层处理,而不是默默排队等着超时。
@Bean("workflowExecutor")
public ThreadPoolExecutor workflowExecutor() {
int cpuCount = Runtime.getRuntime().availableProcessors();
return new ThreadPoolExecutor(
cpuCount * 2, // 核心线程:CPU 核数的 2 倍
cpuCount * 4, // 最大线程:CPU 核数的 4 倍
30, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(200),
new ThreadFactoryBuilder().setNameFormat("workflow-%d").build(),
new ThreadPoolExecutor.AbortPolicy() // 满了就抛异常,快速失败
);
}
第三种是定时任务线程池,用于心跳、清理等周期性任务,定时任务数量有限且可预测,核心线程设 4 个就够用了。
@Bean("scheduledExecutor")
public ScheduledExecutorService scheduledExecutor() {
return new ScheduledThreadPoolExecutor(
4, // 4 个核心线程足够
new ThreadFactoryBuilder().setNameFormat("scheduled-%d").build()
);
}
追问 1:你对参数的配置有哪些依据?
对于 IO 密集型任务(网络请求、文件读写),由于线程大部分时间在等待,可以多开些线程,一般设 CPU 核数的 2-4 倍甚至更多。
对于 CPU 密集型任务(计算、压缩),由于线程一直在干活,开太多反而增加切换开销,一般设 CPU 核数或 +1。
队列容量太小容易触发拒绝,太大会导致任务堆积、响应延迟。要根据任务处理速度和可接受的延迟来定。拒绝策略看业务容忍度。可以丢的任务用 DiscardPolicy,重要任务用 CallerRunsPolicy 或 AbortPolicy。
还有一点很重要,生产环境一定要给线程池命名,出问题时看线程 dump 才知道是哪个池的线程卡住了。
2. 为什么用 SynchronousQueue 而不是 LinkedBlockingQueue?
考察点:阻塞队列选型
参考答案:
SynchronousQueue 是一个"没有容量"的队列,每个 put 必须等待一个 take。选 SynchronousQueue 的原因是:
-
工作流执行对延迟敏感,不希望任务在队列里等着
-
队列满了就创建新线程,能快速应对突发流量
-
如果用 LinkedBlockingQueue,任务可能堆积很多才发现系统过载
对比:
| 特性 | SynchronousQueue | LinkedBlockingQueue |
|---|---|---|
| 容量 | 0 | 可配置(默认 Integer.MAX_VALUE) |
| 入队 | 必须有消费者在等 | 直接入队 |
| 适合场景 | 追求低延迟 | 允许排队 |
| 风险 | 可能创建很多线程 | 可能堆积很多任务 |
// SynchronousQueue:来一个任务,要么有线程处理,要么创建新线程
new ThreadPoolExecutor(10, 50, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());
// LinkedBlockingQueue:来一个任务,先排队
new ThreadPoolExecutor(10, 50, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
3. CompletableFuture 和 Future 有什么区别?你是怎么用的?
考察点:异步编程
参考答案:
Future 是 Java 5 引入的,只能阻塞等待结果:
Future future = executor.submit(() -> "result");
String result = future.get(); // 阻塞等待
CompletableFuture 是 Java 8 引入的,支持链式调用和回调:
CompletableFuture.supplyAsync(() -> "step1")
.thenApply(s -> s + " step2")
.thenAccept(System.out::println)
.exceptionally(ex -> { log.error(ex); return null; });
主要区别:
| 特性 | Future | CompletableFuture |
|---|
真诚点赞 诚不我欺
回复