自定义线程池:拒绝策略的实现思路

在手写线程池的过程中,拒绝策略是保障线程池稳定运行的关键兜底机制。它解决了任务过载时的处理问题,本文从设计初衷、核心思想到代码实现,完整梳理拒绝策略的设计与落地思路。


一、设计初衷:为什么需要拒绝策略?

线程池在运行时存在两个硬性限制:

  • 核心线程数量固定,不能无限制创建新线程
  • 阻塞队列容量有限,不能无限缓存待执行任务

当新任务提交时,若出现 核心线程已满 + 阻塞队列已满 的情况,任务既无法立即执行,也无法暂存队列。
为了避免程序阻塞、任务丢失或系统资源耗尽,必须提供一套灵活的兜底处理方案,这便是拒绝策略的设计意义。

二、核心设计思想

拒绝策略采用 策略模式 进行设计,遵循两个核心原则:

  1. 职责分离,解耦实现
    • 线程池:只负责判断任务是否达到拒绝条件
    • 阻塞队列:只负责判断自身容量是否已满
    • 拒绝逻辑:由外部自定义,不硬编码在线程池内部
  2. 灵活可扩展
    切换或新增拒绝策略时,无需修改线程池与队列源码,只需替换外部实现逻辑即可。

这套设计思路,与 JDK 原生 ThreadPoolExecutor 的拒绝策略设计完全一致,理解后可以无缝对接原生线程池学习。


三、分步实现思路

1. 定义函数式接口:统一拒绝策略规范

先通过接口约定拒绝行为,保证所有拒绝策略都有统一的调用方式。
同时使用函数式接口,支持直接使用 Lambda 表达式实现策略。

@FunctionalInterface
interface RejectPolicy<T> {
    /**
     * 拒绝策略核心方法
     * @param queue 阻塞队列,可用于再次尝试入队
     * @param task  被拒绝的任务
     */
    void reject(BlockingQueue<T> queue, T task);
}

2. 线程池持有拒绝策略

拒绝策略属于线程池配置,由外部传入,线程池只负责保存与传递,不关心具体实现。

class ThreadPool {
    // 拒绝策略对象
    private RejectPolicy<Runnable> rejectPolicy;

    public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, 
                      int queueCapacity, RejectPolicy<Runnable> rejectPolicy) {
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        this.taskQueue = new BlockingQueue<>(queueCapacity);
        // 注入外部自定义拒绝策略
        this.rejectPolicy = rejectPolicy;
    }
}

3. 任务提交:判断是否触发拒绝流程

在线程池执行任务的入口方法中,对任务进行分流:

  • 工作线程数未达到核心线程数:直接新建线程执行任务
  • 工作线程数已满:尝试将任务加入阻塞队列
  • 队列也满:由队列触发拒绝策略
public void execute(Runnable task) {
    synchronized (workers) {
        // 线程未满:直接创建工作线程
        if (workers.size() < coreSize) {
            Worker worker = new Worker(task);
            workers.add(worker);
            worker.start();
        } 
        // 线程已满:尝试入队,队列满则执行拒绝策略
        else {
            taskQueue.tryPut(rejectPolicy, task);
        }
    }
}

4. 阻塞队列:最终判断并执行拒绝

队列自身最清楚容量状态,在 tryPut 方法中完成最终判断:
队列未满 → 正常添加任务;
队列已满 → 执行外部传入的拒绝策略。

public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
    lock.lock();
    try {
        if (queue.size() == capacity) {
            // 队列已满,执行拒绝策略
            rejectPolicy.reject(this, task);
        } else {
            // 正常入队,并唤醒等待的工作线程
            queue.addLast(task);
            emptyWaitSet.signal();
        }
    } finally {
        lock.unlock();
    }
}

5. 外部自定义拒绝策略

使用线程池时,通过 Lambda 灵活定义拒绝行为,常见策略包括:

  • 无限阻塞等待入队
  • 带超时时间等待入队
  • 直接丢弃任务
  • 抛出异常通知上层
  • 由提交任务的线程自己执行
ThreadPool threadPool = new ThreadPool(1, 1000, TimeUnit.MILLISECONDS, 1,
    (queue, task) -> {
        // 自定义拒绝逻辑:调用者线程自行执行
        task.run();
    });

四、通用任务执行分流逻辑

抛开具体代码参数,拒绝策略的通用执行流程可以总结为:

  1. 新任务提交到线程池
  2. 判断当前工作线程数是否小于核心线程数
    • 是:创建新线程执行任务
    • 否:尝试将任务加入阻塞队列
  3. 队列加入成功:等待工作线程消费
  4. 队列加入失败(队列已满):触发拒绝策略
  5. 执行自定义兜底逻辑(丢弃、抛异常、调用者执行等)

五、设计优势

  1. 解耦彻底
    线程池、阻塞队列、拒绝逻辑三者相互独立,互不侵入,便于维护和扩展。
  2. 扩展性强
    新增拒绝策略只需新增实现,无需改动线程池核心代码。
  3. 灵活性高
    支持多种拒绝行为,可根据业务场景自由选择。
  4. 贴近工业级实现
    设计思想对齐 JDK 原生线程池,学习成本低、实用性强。

六、总结

拒绝策略的本质,是线程池面对任务过载时的兜底保护机制。
通过策略模式将“是否拒绝”与“如何拒绝”分离,既保证了线程池的稳定性,又保留了极高的灵活性。
理解这一设计思路,不仅能看懂手写线程池,也能更深入掌握 Java 原生线程池的工作原理。