任务特别多,线程池队列满了怎么办?不能拒绝!

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

  1. 线程池原理掌握度:面试官不仅仅是想知道你会不会配置线程池,更是想知道你是否理解线程池的执行流程、队列机制以及 4 种拒绝策略的触发时机和各自特点。

  2. 生产问题解决能力:这道题源于真实的生产场景,考察你在面对 "任务不能丢" 这种业务约束时,能否给出多层次的解决方案,而不是只会说 "换更大的队列"。

  3. 系统设计思维:考察你是否具备从线程池层面 → 架构层面 → 中间件层面的多维度思考能力,以及是否理解 "背压(Backpressure)" 机制在高并发系统中的重要性。

核心答案

线程池队列满且不能拒绝任务,需要 分层解决

层面方案核心思路适用场景
线程池层CallerRunsPolicy调用者线程执行,自带背压中小规模、任务波动
线程池层动态扩容调整核心/最大线程数、队列容量可预测的流量高峰
架构层消息队列削峰MQ 异步解耦 + 持久化高并发、大流量
架构层任务持久化 + 重试先存 DB,后台重试提交任务绝对不能丢
系统层限流降级Sentinel 等限流组件保护系统稳定性

一句话总结CallerRunsPolicy 是最简单的兜底,消息队列是最可靠的方案,任务持久化是最后的防线。

深度解析

一、线程池拒绝策略回顾

当线程池无法接受新任务时(线程全忙 + 队列已满),会触发拒绝策略:

线程池任务提交流程线程池任务提交流程

上图展示了线程池任务提交的完整流程。整体分为以下几个阶段:

  1. 核心线程判断:优先检查是否有空闲的核心线程,有则直接执行,这是最快的路径。

  2. 队列入队:核心线程全忙时,任务进入阻塞队列等待。队列是有界的,满则进入下一步。

  3. 非核心线程创建:队列满后,检查是否达到最大线程数,未达则创建非核心线程立即执行。

  4. 拒绝策略触发:线程全忙 + 队列已满 + 无法创建新线程,触发拒绝策略。

关键点在于:拒绝策略触发意味着系统已满载,需要通过合理的策略来应对。

4 种内置拒绝策略

策略行为问题
AbortPolicy抛异常❌ 任务丢失
DiscardPolicy静默丢弃❌ 任务丢失
DiscardOldestPolicy丢弃队首❌ 任务丢失
CallerRunsPolicy调用者执行✅ 不丢任务

二、方案一:CallerRunsPolicy(调用者运行)

这是最简单有效的方案,没有之一:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    10,                         // 核心线程数
    50,                         // 最大线程数
    60L, TimeUnit.SECONDS,      // 空闲线程存活时间
    new LinkedBlockingQueue<>(1000),  // 有界队列
    new ThreadPoolExecutor.CallerRunsPolicy()  // 关键!
);

原理分析

CallerRunsPolicy 工作原理CallerRunsPolicy 工作原理

上图展示了 CallerRunsPolicy 的工作原理:

  1. 正常情况:任务提交成功,由线程池异步执行,请求线程立即返回。

  2. 满载情况:线程池无法接收任务,调用 CallerRunsPolicy,任务回退给提交者执行。

  3. 背压机制:请求线程被迫执行任务期间被阻塞,自然减缓了任务提交速度。

关键点在于:这是一种 "自动调节" 机制,无需额外代码,天然实现了流量控制。

优点

  • 零代码改动,配置即可
  • 自带背压,自动降速
  • 任务不丢失

缺点

  • 可能阻塞业务线程(如 Tomcat 线程)
  • 极端情况下可能拖垮服务

适用场景:任务量有波动但整体可控,允许一定的响应延迟。

三、方案二:动态扩容线程池

线程池参数支持运行时调整:

public class DynamicThreadPool {

    private final ThreadPoolExecutor executor;

    public void resize(int coreSize, int maxSize, int queueCapacity) {
        executor.setCorePoolSize(coreSize);
        executor.setMaximumPoolSize(maxSize);

        // 队列扩容需要自定义实现
        if (executor.getQueue() instanceof ResizableCapacityLinkedBlockingQueue) {
            ((ResizableCapacityLinkedBlockingQueue<Runnable>) executor.getQueue())
                .setCapacity(queueCapacity);
        }
    }
}

JDK 原生支持

  • setCorePoolSize()
  • setMaximumPoolSize()
  • 队列容量 ❌(需要自定义可扩容队列)

自定义可扩容队列

public class ResizableCapacityLinkedBlockingQueue<E> extends LinkedBlockingQueue<E> {

    public ResizableCapacityLinkedBlockingQueue(int capacity) {
        super(capacity);
    }

    // 通过反射修改父类的 capacity 字段
    public synchronized void setCapacity(int capacity) {
        try {
            Field field = LinkedBlockingQueue.class.getDeclaredField("capacity");
            field.setAccessible(true);
            field.set(this, capacity);
        } catch (Exception e) {
            throw new RuntimeException("扩容失败", e);
        }
    }
}

优点:灵活应对流量高峰 缺点:有上限,治标不治本

四、方案三:消息队列削峰(推荐)

引入消息队列是生产环境最常用的方案:

消息队列削峰架构消息队列削峰架构

上图展示了消息队列削峰的完整架构:

  1. 生产者:业务系统快速将任务推送到 MQ,立即返回,实现异步解耦。

  2. MQ 队列:作为缓冲区,高峰期积压任务,低峰期逐步消化,实现 "削峰填谷"。

  3. 持久化:MQ 的持久化机制保证任务不丢失,即使消费者宕机也能恢复。

  4. 消费者:根据线程池处理能力控制消费速率,避免被压垮。

关键点在于:MQ 是流量洪峰和系统处理能力之间的 "蓄水池",是高并发系统的标配。

核心代码

// 生产者:提交任务到 MQ
public void submitTask(Task task) {
    rabbitTemplate.convertAndSend("task.queue", task);
    // 快速返回,不阻塞
}

// 消费者:按能力消费
@RabbitListener(queues = "task.queue")
public void consume(Task task) {
    // 使用 CallerRunsPolicy 兜底
    executor.execute(() -> process(task));
}

优点

  • 异步解耦,快速响应
  • 削峰填谷,保护系统
  • 持久化,任务不丢
  • 可横向扩展消费者

缺点

  • 架构复杂度增加
  • 有延迟(异步处理)

五、方案四:任务持久化 + 重试机制

这是最后的防线,保证任务 绝对不丢

public class PersistentTaskExecutor {

    private final ThreadPoolExecutor executor;
    private final TaskRepository taskRepository;  // 数据库

    public void submit(Task task) {
        try {
            executor.execute(() -> {
                process(task);
                // 成功后删除持久化记录
                taskRepository.delete(task.getId());
            });
        } catch (RejectedExecutionException e) {
            // 持久化到数据库
            taskRepository.save(task);
            log.warn("任务已持久化,等待重试: {}", task.getId());
        }
    }

    // 后台定时任务:重试持久化的任务
    @Scheduled(fixedDelay = 5000)
    public void retryPersistedTasks() {
        List<Task> tasks = taskRepository.findPendingTasks(100);
        for (Task task : tasks) {
            submit(task);  // 重新提交
        }
    }
}

流程图

任务持久化 + 重试机制任务持久化 + 重试机制

上图展示了任务持久化重试的完整流程:

  1. 正常路径:线程池接受任务,执行成功后直接完成。

  2. 异常路径:线程池拒绝任务,将任务持久化到数据库,保证不丢失。

  3. 重试机制:后台定时任务扫描待处理任务,重新提交到线程池。

  4. 清理机制:任务执行成功后删除持久化记录,避免重复处理。

关键点在于:数据库是最后的保障,即使系统重启也能恢复任务,实现 "最终一致性"。

六、方案五:监控预警 + 限流降级

监控线程池状态

public void monitorThreadPool() {
    BlockingQueue<Runnable> queue = executor.getQueue();

    int activeCount = executor.getActiveCount();     // 活跃线程数
    int queueSize = queue.size();                    // 队列任务数
    int queueCapacity = queue.remainingCapacity();   // 队列剩余容量

    // 队列使用率超过 80% 预警
    double usage = (double) queueSize / (queueSize + queueCapacity);
    if (usage > 0.8) {
        log.warn("线程池队列使用率: {}%", usage * 100);
        // 触发告警、自动扩容、限流等
    }
}

配合 Sentinel 限流

// 在任务提交前限流
public void submit(Task task) {
    try (Entry entry = SphU.entry("taskSubmit")) {
        executor.execute(() -> process(task));
    } catch (BlockException e) {
        // 被限流,走降级逻辑(如持久化)
        taskRepository.save(task);
    }
}

七、生产级完整方案

将以上方案组合,形成完整的防护体系:

public class ProductionTaskExecutor {

    private final ThreadPoolExecutor executor;
    private final MessageQueue mq;
    private final TaskRepository db;

    public void submit(Task task) {
        // 第一层:线程池 + CallerRunsPolicy
        try {
            executor.execute(() -> process(task));
            return;
        } catch (RejectedExecutionException e) {
            log.warn("线程池已满,进入降级流程");
        }

        // 第二层:发送到 MQ
        try {
            mq.send(task);
            return;
        } catch (Exception e) {
            log.error("MQ 发送失败,进入持久化");
        }

        // 第三层:持久化到数据库
        db.save(task);
    }
}

面试高频追问

  1. 追问一CallerRunsPolicy 会不会把 Tomcat 线程池拖垮?

    :有可能。如果任务执行时间很长,会导致 Tomcat 线程被阻塞,影响其他请求。解决方案是配合超时机制,或者任务执行前先判断剩余时间。

  2. 追问二:如何实现一个支持动态调整的线程池?

    :继承 ThreadPoolExecutor,暴露 resize() 方法,通过反射支持队列容量调整。开源方案可用 Hippo4j 动态线程池框架。

  3. 追问三:消息队列积压了 100 万条消息,怎么处理?

    • 临时扩容消费者实例
    • 批量消费提高吞吐
    • 持久化后异步处理
    • 根本上优化任务处理逻辑

常见面试变体

  • 变体一:"如何设计一个任务不能丢失的异步处理系统?"
  • 变体二:"线程池满了,怎么优雅降级?"
  • 变体三:"高并发场景下如何保证任务最终执行?"
  • 变体四:"说说你对背压(Backpressure)机制的理解?"

记忆口诀

分层防御:线程池 → 消息队列 → 数据库

兜底策略CallerRuns 是最简,MQ 削峰最常用,DB 持久化最可靠

核心原则:不能拒绝 = 必须持久化 + 最终执行

总结

线程池队列满且不能拒绝任务,需要从线程池层面(CallerRunsPolicy、动态扩容)、架构层面(消息队列削峰、任务持久化重试)、系统层面(限流降级)多维度解决。生产环境推荐组合使用CallerRunsPolicy 作为第一道防线 + MQ 削峰作为常规方案 + DB 持久化作为最后保障,确保任务最终执行。