Skip to content

线程池队列与拒绝策略

线程池的队列和拒绝策略决定了系统过载时是“排队等待、临时扩容、直接失败、反压还是丢弃”。这部分如果没学明白,线程池很容易把服务拖垮。

队列为什么重要

线程池不是只靠线程数控制并发。队列决定任务能积压多少,也决定 maximumPoolSize 什么时候生效。

mermaid
flowchart TD
    A["任务提交"] --> B{"核心线程已满"}
    B -- "否" --> C["核心线程执行"]
    B -- "是" --> D{"队列是否能放入"}
    D -- "能" --> E["任务排队"]
    D -- "不能" --> F["尝试创建非核心线程"]

如果队列是无界的,任务几乎总能放进去,那么线程池很少创建非核心线程,maximumPoolSize 就形同虚设。

常见队列对比

队列是否有界特点适合场景
ArrayBlockingQueue有界数组结构,容量固定生产常用,容量清晰
LinkedBlockingQueue可有界也可无界链表结构,默认 Integer.MAX_VALUE固定线程池常见,但要显式设置容量
SynchronousQueue不存储任务提交任务必须直接交给线程高弹性任务,但线程数风险高
PriorityBlockingQueue默认无界按优先级执行优先任务,但要防堆积
DelayQueue无界到期后才能取出延迟任务,不常作为普通线程池队列

ArrayBlockingQueue

ArrayBlockingQueue 是有界队列,容量固定,适合生产中明确限制积压数量。

java
new ThreadPoolExecutor(
        4,
        8,
        60,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<Runnable>(200)
);

优点是容量可控,缺点是容量需要估算。容量太小容易频繁拒绝,容量太大又会让请求排队太久,用户看到超时。

LinkedBlockingQueue

LinkedBlockingQueue 如果不传容量,默认容量非常大。

java
new LinkedBlockingQueue<Runnable>();

这就是 Executors.newFixedThreadPool 的风险:线程数固定,但队列近似无界。流量持续大于处理能力时,任务会不断堆积,内存持续上涨。

正确做法是显式传容量:

java
new LinkedBlockingQueue<Runnable>(500);

SynchronousQueue

SynchronousQueue 不保存任务。每个任务必须直接交给一个工作线程。

mermaid
flowchart TD
    A["提交任务"] --> B{"是否有空闲线程接收"}
    B -- "有" --> C["直接交给线程"]
    B -- "没有" --> D["尝试创建新线程"]
    D --> E{"达到最大线程数"}
    E -- "否" --> F["新线程执行"]
    E -- "是" --> G["拒绝"]

newCachedThreadPool 使用的就是这种队列,并且最大线程数非常大。短任务、低风险场景可以用,但生产核心业务要谨慎,否则突发流量会创建大量线程。

拒绝策略总览

mermaid
flowchart TD
    A["线程池和队列都满"] --> B["拒绝策略"]
    B --> C["AbortPolicy 抛异常"]
    B --> D["CallerRunsPolicy 调用者执行"]
    B --> E["DiscardPolicy 静默丢弃"]
    B --> F["DiscardOldestPolicy 丢弃最老任务"]
    B --> G["自定义策略 记录日志和告警"]

AbortPolicy

默认策略,直接抛出 RejectedExecutionException

适合核心任务,因为核心任务不能静默丢弃,必须让调用方知道失败。

java
new ThreadPoolExecutor.AbortPolicy()

如果业务没有捕获这个异常,可能导致接口直接报错。报错不是坏事,至少比静默丢任务更容易发现。

CallerRunsPolicy

让提交任务的线程自己执行任务。

mermaid
flowchart TD
    A["业务线程提交任务"] --> B["线程池已满"]
    B --> C["业务线程自己执行任务"]
    C --> D["提交速度变慢"]
    D --> E["形成反压"]

适合允许变慢但不能丢的任务,例如内部采集、批量处理、异步入库。它的好处是自动降低提交速度,坏处是调用线程会被拖慢。

DiscardPolicy

直接丢弃任务,不抛异常。

适合极少数允许丢失的非核心任务,例如埋点、统计计数、临时刷新。订单、支付、库存、消息通知不能用这个策略。

自定义拒绝策略

生产中常见做法是记录日志、打监控、写失败表或降级处理。

java
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 或采样可接受少量丢失

面试标准回答

问题:线程池常见队列和拒绝策略有哪些?

标准回答:

常见队列有 ArrayBlockingQueueLinkedBlockingQueueSynchronousQueuePriorityBlockingQueue。生产中通常建议使用有界队列,避免任务无限堆积。拒绝策略有 AbortPolicyCallerRunsPolicyDiscardPolicyDiscardOldestPolicy,核心任务一般不能静默丢弃,可以使用抛异常或自定义策略;需要反压的场景可以使用 CallerRunsPolicy

本章小结

线程池过载时的表现,主要由队列和拒绝策略决定。无界队列会隐藏问题直到内存耗尽,静默丢弃会让业务数据不一致。生产线程池必须明确容量边界和过载处理方式。