线程池队列与拒绝策略
线程池的队列和拒绝策略决定了系统过载时是“排队等待、临时扩容、直接失败、反压还是丢弃”。这部分如果没学明白,线程池很容易把服务拖垮。
队列为什么重要
线程池不是只靠线程数控制并发。队列决定任务能积压多少,也决定 maximumPoolSize 什么时候生效。
flowchart TD
A["任务提交"] --> B{"核心线程已满"}
B -- "否" --> C["核心线程执行"]
B -- "是" --> D{"队列是否能放入"}
D -- "能" --> E["任务排队"]
D -- "不能" --> F["尝试创建非核心线程"]如果队列是无界的,任务几乎总能放进去,那么线程池很少创建非核心线程,maximumPoolSize 就形同虚设。
常见队列对比
| 队列 | 是否有界 | 特点 | 适合场景 |
|---|---|---|---|
ArrayBlockingQueue | 有界 | 数组结构,容量固定 | 生产常用,容量清晰 |
LinkedBlockingQueue | 可有界也可无界 | 链表结构,默认 Integer.MAX_VALUE | 固定线程池常见,但要显式设置容量 |
SynchronousQueue | 不存储任务 | 提交任务必须直接交给线程 | 高弹性任务,但线程数风险高 |
PriorityBlockingQueue | 默认无界 | 按优先级执行 | 优先任务,但要防堆积 |
DelayQueue | 无界 | 到期后才能取出 | 延迟任务,不常作为普通线程池队列 |
ArrayBlockingQueue
ArrayBlockingQueue 是有界队列,容量固定,适合生产中明确限制积压数量。
new ThreadPoolExecutor(
4,
8,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(200)
);优点是容量可控,缺点是容量需要估算。容量太小容易频繁拒绝,容量太大又会让请求排队太久,用户看到超时。
LinkedBlockingQueue
LinkedBlockingQueue 如果不传容量,默认容量非常大。
new LinkedBlockingQueue<Runnable>();这就是 Executors.newFixedThreadPool 的风险:线程数固定,但队列近似无界。流量持续大于处理能力时,任务会不断堆积,内存持续上涨。
正确做法是显式传容量:
new LinkedBlockingQueue<Runnable>(500);SynchronousQueue
SynchronousQueue 不保存任务。每个任务必须直接交给一个工作线程。
flowchart TD
A["提交任务"] --> B{"是否有空闲线程接收"}
B -- "有" --> C["直接交给线程"]
B -- "没有" --> D["尝试创建新线程"]
D --> E{"达到最大线程数"}
E -- "否" --> F["新线程执行"]
E -- "是" --> G["拒绝"]newCachedThreadPool 使用的就是这种队列,并且最大线程数非常大。短任务、低风险场景可以用,但生产核心业务要谨慎,否则突发流量会创建大量线程。
拒绝策略总览
flowchart TD
A["线程池和队列都满"] --> B["拒绝策略"]
B --> C["AbortPolicy 抛异常"]
B --> D["CallerRunsPolicy 调用者执行"]
B --> E["DiscardPolicy 静默丢弃"]
B --> F["DiscardOldestPolicy 丢弃最老任务"]
B --> G["自定义策略 记录日志和告警"]AbortPolicy
默认策略,直接抛出 RejectedExecutionException。
适合核心任务,因为核心任务不能静默丢弃,必须让调用方知道失败。
new ThreadPoolExecutor.AbortPolicy()如果业务没有捕获这个异常,可能导致接口直接报错。报错不是坏事,至少比静默丢任务更容易发现。
CallerRunsPolicy
让提交任务的线程自己执行任务。
flowchart TD
A["业务线程提交任务"] --> B["线程池已满"]
B --> C["业务线程自己执行任务"]
C --> D["提交速度变慢"]
D --> E["形成反压"]适合允许变慢但不能丢的任务,例如内部采集、批量处理、异步入库。它的好处是自动降低提交速度,坏处是调用线程会被拖慢。
DiscardPolicy
直接丢弃任务,不抛异常。
适合极少数允许丢失的非核心任务,例如埋点、统计计数、临时刷新。订单、支付、库存、消息通知不能用这个策略。
自定义拒绝策略
生产中常见做法是记录日志、打监控、写失败表或降级处理。
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
class LoggingRejectPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable task, ThreadPoolExecutor executor) {
System.out.println("线程池拒绝任务 active="
+ executor.getActiveCount()
+ ", queue=" + executor.getQueue().size());
throw new RuntimeException("系统繁忙,请稍后再试");
}
}注意:拒绝策略里不要执行耗时逻辑,否则满载时会进一步拖慢系统。
商业场景选择
| 场景 | 推荐队列 | 推荐拒绝策略 | 原因 |
|---|---|---|---|
| 订单核心处理 | 有界队列 | AbortPolicy 或自定义明确失败 | 不能丢任务 |
| 批量采集 | 有界队列 | CallerRunsPolicy | 允许变慢,形成反压 |
| 日志异步写入 | 有界队列 | 自定义降级或丢弃低级别日志 | 不能影响主流程 |
| 报表导出 | 有界队列 | 自定义返回“排队中/繁忙” | 用户可稍后查看 |
| 埋点统计 | 有界队列 | DiscardPolicy 或采样 | 可接受少量丢失 |
面试标准回答
问题:线程池常见队列和拒绝策略有哪些?
标准回答:
常见队列有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue。生产中通常建议使用有界队列,避免任务无限堆积。拒绝策略有 AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy,核心任务一般不能静默丢弃,可以使用抛异常或自定义策略;需要反压的场景可以使用 CallerRunsPolicy。
本章小结
线程池过载时的表现,主要由队列和拒绝策略决定。无界队列会隐藏问题直到内存耗尽,静默丢弃会让业务数据不一致。生产线程池必须明确容量边界和过载处理方式。
