如果你已经完整读完前 8 篇,那么你现在一定会有一个感觉:
线程池真正危险的地方,不是线程用完,而是“任务源源不断地进来”。
这正是本篇的主题:背压(BackPressure)。
一、先给一句“架构级结论”
线程池的本质不是并发执行器,而是“生产者—消费者系统”。
只要生产速度 > 消费速度,系统一定会出问题。
线程池不是万能的,它解决的是“并行消费能力”,
它解决不了“无限生产”的问题。
二、什么是背压?先抛开线程池
背压不是线程池独有的概念,它是一个通用系统设计思想:
当下游处理能力不足时,必须反过来“压住”上游的生产速度。
如果没有背压,系统会发生什么?
队列无限增长
内存膨胀
延迟指数级上升
最终 OOM / 服务崩溃
线程池如果没有背压机制,就是一个“慢性死亡系统”。
三、线程池里的“生产者—消费者模型”
在线程池中:
生产者:提交任务的线程(HTTP 请求线程、MQ 消费线程、定时任务等)
消费者:线程池中的 worker 线程
缓冲区:任务队列(BlockingQueue)
核心矛盾只有一个:
任务提交速度 > 线程池处理速度
一旦长期如此,任何线程池都会崩。
四、JDK 给你的四种拒绝策略,其实就是四种“背压选择”
很多人只把拒绝策略当成“满了怎么办”,
但从系统视角看,它们本质是背压策略。
1️⃣ AbortPolicy —— 直接失败(强硬型)
throw new RejectedExecutionException();- 直接把压力甩给上游
- 适合:强一致性、不能丢任务
- 缺点:用户体验差,容易雪崩
2️⃣ DiscardPolicy —— 静默丢弃(最危险)
// 什么都不做
- 表面稳定,实际业务丢数据
- 生产环境极不推荐
3️⃣ DiscardOldestPolicy —— 丢旧保新(有选择的背压)
- 适合只关心“最新状态”的任务
- 如 UI 刷新、状态同步、心跳
4️⃣ CallerRunsPolicy —— 天然背压(最优雅)
// 让提交任务的线程自己执行
这一条非常重要:
CallerRunsPolicy 会“拖慢生产者本身”,从而自动降低生产速度。
这就是天然背压。
五、为什么 CallerRunsPolicy 是“最像 RxJava 背压”的?
你之前说过一句非常关键的话:
“这和 RxJava 的背压思路有点像。”
你这个直觉是完全正确的。
RxJava 背压的本质
- 下游消费不过来
- 上游不能无限发射
- 通过 request(n) / 阻塞 / 丢弃 / 缓存 等方式调节
CallerRunsPolicy 的本质
- 线程池消费不过来
- 不再接受新任务
- 让上游线程“自己干活”,自然变慢
两者本质一致:
消费能力反向限制生产速度。
六、一个非常直观的例子(强烈推荐你放进博客)
ThreadPoolExecutor pool = new ThreadPoolExecutor( 2, 2, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2), new ThreadPoolExecutor.CallerRunsPolicy() ); for (int i = 0; i < 10; i++) { int id = i; pool.execute(() -> { System.out.println( "task " + id + " run by " + Thread.currentThread().getName() ); try { Thread.sleep(1000); } catch (InterruptedException ignored) {} }); }你会观察到:
- 一开始任务在 pool 线程跑
- 当池子 + 队列满了
- main 线程会开始执行任务
这意味着:
任务提交速度被强制拖慢了。
七、背压不是“丢任务”,而是“系统自保”
很多人对背压有误解:
❌ 背压 = 丢任务
❌ 背压 = 系统变慢
正确理解是:
背压 = 用延迟换稳定,用局部变慢换整体不崩。
线程池如果没有背压,最终只会:
- 全局阻塞
- 内存爆炸
- JVM 崩溃
八、生产系统中常见的 4 种背压组合策略
✔ 1)有界队列 + CallerRunsPolicy(最通用)
默认推荐
适合绝大多数后端 / 客户端任务
✔ 2)队列限长 + 超时等待 + 降级
if (queue.size() > threshold) { // 拒绝低优先级任务 }✔ 3)丢旧保新(DiscardOldestPolicy)
- 适合“状态同步类任务”
- 不适合金融 / 交易 / 核心业务
✔ 4)背压 + 限流器(RateLimiter)
- 线程池背压 + 入口限流
- 双重保险
九、为什么“无界队列 = 没有背压”?
这句话你一定要记住:
无界队列意味着:无论系统多慢,生产者都可以无限快。
这等价于:
- 没有背压
- 没有保护
- 只是在“延迟爆炸前排队”
这也是为什么生产环境强烈不推荐 Executors.newFixedThreadPool()。
十、本篇总结(可直接放文末)
- 线程池本质是生产者—消费者系统
- 背压是系统稳定性的核心机制
- 拒绝策略本质上就是背压策略
- CallerRunsPolicy 是最自然、最安全的背压方式
- 无界队列 = 没有背压 = 慢性死亡
- 用延迟换稳定,是工程系统的基本原则