防丢失关键在于可控的饱和处理而非避免饱和;须用有界队列+显式拒绝策略,首选CallerRunsPolicy实现反压,不可丢任务需自定义持久化策略,并配套监控预警。

核心业务中防止请求丢失,关键不是避免线程池饱和,而是让饱和时的处理方式可控、可预期、不丢数据。饱和本身是系统承压的正常信号,真正危险的是默认 AbortPolicy 导致异常未捕获、任务静默消失。
明确触发饱和的真实条件
线程池拒绝策略不会在“队列看起来快满”或“线程数接近最大值”时触发。它只在三个条件**同时成立**时才生效:
- 线程池处于 RUNNING 状态(非 shutdown/shutdownNow)
- 工作队列是有界队列(如 ArrayBlockingQueue),且已满
- 当前线程数已达 maximumPoolSize
注意:若用 LinkedBlockingQueue(无界队列),即使线程数已达 maximumPoolSize,新任务仍会持续入队——这看似“不拒绝”,实则埋下 OOM 隐患。所以防丢失的第一步,是**必须用有界队列 + 显式配置拒绝策略**。
优先选用 CallerRunsPolicy(调用者执行)
这是核心业务防丢失最实用、开箱即用的策略。它不抛异常、不丢任务,而是把当前被拒的任务交还给提交线程(比如 Tomcat 的 worker 线程)去同步执行。
- 效果上相当于给上游加了一层天然反压:调用线程忙于执行任务,自然降低后续 submit 速率
- 适用于 Web 请求类场景——只要业务逻辑执行时间可控(例如 ≤ 200ms),用户端最多感知为响应稍慢,而非 500 或超时
- 配置方式简单:
new ThreadPoolExecutor(..., new ThreadPoolExecutor.CallerRunsPolicy())
对绝对不可丢的任务,必须自定义持久化策略
CallerRunsPolicy 虽好,但依赖调用线程能及时执行。若请求线程本身不能长时间阻塞(如 Netty EventLoop、定时任务线程),或任务执行耗时不可控,则需兜底方案:
- 实现 RejectedExecutionHandler 接口,在
rejectedExecution()中将任务序列化后写入 Kafka / RocketMQ / 数据库 - 搭配独立的补偿服务,监听消息/轮询数据库,等线程池负载下降后重新投递
- 注意任务对象必须可序列化,且幂等设计要前置——重试可能多次触发
配套监控与容量水位预警
再好的策略也需可观测性支撑。仅靠拒绝策略被动应对远远不够:
- 暴露并采集关键指标:活跃线程数、队列剩余容量、
getRejectedExecutionCount()(需继承 ThreadPoolExecutor 自定义计数器) - 设置告警阈值:例如队列使用率 > 80% 持续 1 分钟,或每分钟拒绝数 > 5 次,立即通知负责人
- 压测时重点验证:在目标 QPS 下,线程池是否稳定在合理水位,拒绝率是否趋近于 0
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











