高并发与线程池¶
一句话:线程池是高并发系统的"流量调节阀",配合熔断降级与隔离,是把万级 QPS 压成系统可承受负载的核心手段。
概念¶
高并发系统的本质矛盾是:瞬时请求量远大于系统可承载的处理能力。单机 QPS 物理上限由 CPU、内存、IO 与外部依赖 RTT 共同决定,盲目扩容成本不可控,正确做法是"用有限的资源扛住峰值"。
Java 处理高并发的三件套是:线程池(控制并发度与背压)、隔离与熔断降级(防止故障扩散)、缓存(挡住读流量)。这三者不是孤立的:线程池决定"同时能处理多少请求",熔断决定"扛不住时丢谁",缓存决定"有多少请求根本不需要落到后端"。
在电商大促交易链路这类场景,峰值可达万级 QPS,但下游 MySQL、第三方支付、库存服务的能力是有限的。直接把万级流量灌过去必然雪崩,所以必须在线程池层做隔离、在入口层做熔断、在数据层做多级缓存,三者配合才能保住 99.99% 可用性。
原理¶
线程池 7 参数¶
ThreadPoolExecutor 的 7 个核心参数:
| 参数 | 含义 | 调参要点 |
|---|---|---|
corePoolSize |
核心线程数 | CPU 密集型 ≈ N+1;IO 密集型 ≈ N×(1+等待/计算),常为 2N~10N |
maximumPoolSize |
最大线程数 | 兜底上限,突发流量扩容边界 |
keepAliveTime |
空闲存活时间 | 非核心线程空闲多久回收 |
unit |
时间单位 | 配合 keepAliveTime |
workQueue |
任务队列 | 有界队列(如 ArrayBlockingQueue)防 OOM;无界队列是危险源 |
threadFactory |
线程工厂 | 自定义命名,便于线程栈排查 |
handler |
拒绝策略 | 队列满+线程满时如何处理新任务 |
4 种拒绝策略¶
| 策略 | 行为 | 适用场景 |
|---|---|---|
AbortPolicy |
抛 RejectedExecutionException |
默认;需显式感知并兜底 |
CallerRunsPolicy |
由提交线程自己执行 | 优雅背压,让上游自动降速 |
DiscardPolicy |
静默丢弃 | 日志/监控等可丢场景 |
DiscardOldestPolicy |
丢弃队列最老任务 | 只关心最新(如行情推送) |
提交与执行流程¶
任务提交后的核心判断顺序是:核心线程未满 → 创建核心线程;满了 → 进队列;队列满了 → 创建非核心线程到最大线程数;再满 → 触发拒绝策略。这个顺序经常被答反(误以为先扩到最大线程数再入队),是面试高频陷阱。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
16, // corePoolSize
64, // maximumPoolSize
60L, TimeUnit.SECONDS, // 空闲回收
new ArrayBlockingQueue<>(500), // 有界队列,防止 OOM
new ThreadFactoryBuilder().setNameFormat("order-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 背压
);
线程池隔离¶
不同业务用独立线程池,避免一个慢调用把全局线程池吃光导致全部接口不可用(典型的"连坐雪崩")。隔离方式有两种:
- 线程池隔离:物理隔离,强但开销大,适合核心链路(交易/支付)与非核心(查询/通知)拆分。
- 信号量隔离:轻量,只限流不切换线程,适合内部纯内存调用或高并发读。
Sentinel 熔断降级¶
Sentinel 通过滑动时间窗口 + 资源调用统计实现限流熔断。核心概念:
| 概念 | 作用 |
|---|---|
| 资源 (Resource) | 被保护的代码段/接口 |
| 规则 (Rule) | 限流/熔断/热点/系统规则 |
| 槽位链 (Slot Chain) | 统计、规则判断、降级执行的处理链 |
| 熔断策略 | 慢调用比例、异常比例、异常数 |
熔断的三个状态机:CLOSED(正常)→ OPEN(熔断,快速失败)→ HALF_OPEN(半开探测,放少量请求试探)。恢复探测窗口期内若仍失败,则重新回到 OPEN。
虚拟线程 (JDK 21)¶
JDK 21 正式 GA 的虚拟线程(Project Loom)是高并发的范式转变:
- 传统平台线程:1:1 映射 OS 线程,开销大,单机最多数千。
- 虚拟线程:N:M 映射,由 JVM 调度到少量载体线程上,单机可轻松创建百万级。
虚拟线程特别适合 IO 密集型、高并发、阻塞调用多的场景(如 Web 接口等待 DB/下游)。它让"一个请求一个线程"的同步阻塞写法重新变得高效,不必再写复杂的异步回调。但它不适合 CPU 密集型任务,且使用时要注意避免 synchronized 长时间持有(会钉住载体线程,改用 ReentrantLock)。
实战要点¶
结合大促交易链路(万级 QPS、99.99% 可用)的落地经验:
-
线程池必须自定义,禁用
Executors工具方法:newFixedThreadPool用无界队列、newCachedThreadPool最大线程数是Integer.MAX_VALUE,二者在生产环境都是 OOM 定时炸弹。必须显式new ThreadPoolExecutor并设置有界队列。 -
按业务域隔离线程池:交易核心池、查询池、异步通知池彻底分开。曾遇到第三方支付慢导致通知线程池打满、拖垮下单接口的"连坐"事故,隔离后互不影响。
-
CallerRunsPolicy做优雅背压:队列满时让 Tomcat 工作线程自己执行任务,相当于自动给上游降速,比直接抛异常或丢请求更友好,保住了下游。 -
Sentinel 做兜底熔断:对下游支付、库存等弱依赖设置"慢调用比例熔断"(如 RT > 500ms 比例超 50% 触发),熔断期间走降级逻辑(默认库存、缓存价),保住主链路可用。
-
监控线程池核心指标:活跃线程数、队列堆积、拒绝次数、最大 RT 必须上报 Prometheus,队里堆积是最关键的预警信号——堆积意味着消费跟不上生产,离拒绝不远了。
-
虚拟线程谨慎试点:JDK 21 后可在纯 IO 接口上试点虚拟线程替代池化,但存量
synchronized与ThreadLocal大量使用的代码需先改造,否则载体线程被钉住反而劣化。
本节相关题目¶
| 难度 | 题目 | 链接 |
|---|---|---|
| 基础 | 线程池 7 参数与拒绝策略 | → 题库 |
| 进阶 | P99 劣化如何用线程池+Sentinel 定位治理 | → 题库 |
| 深度 | 设计万级 QPS 大促交易链路 | → 题库 |