跳转至

高并发与线程池

一句话:线程池是高并发系统的"流量调节阀",配合熔断降级与隔离,是把万级 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% 可用)的落地经验:

  1. 线程池必须自定义,禁用 Executors 工具方法newFixedThreadPool 用无界队列、newCachedThreadPool 最大线程数是 Integer.MAX_VALUE,二者在生产环境都是 OOM 定时炸弹。必须显式 new ThreadPoolExecutor 并设置有界队列。

  2. 按业务域隔离线程池:交易核心池、查询池、异步通知池彻底分开。曾遇到第三方支付慢导致通知线程池打满、拖垮下单接口的"连坐"事故,隔离后互不影响。

  3. CallerRunsPolicy 做优雅背压:队列满时让 Tomcat 工作线程自己执行任务,相当于自动给上游降速,比直接抛异常或丢请求更友好,保住了下游。

  4. Sentinel 做兜底熔断:对下游支付、库存等弱依赖设置"慢调用比例熔断"(如 RT > 500ms 比例超 50% 触发),熔断期间走降级逻辑(默认库存、缓存价),保住主链路可用。

  5. 监控线程池核心指标:活跃线程数、队列堆积、拒绝次数、最大 RT 必须上报 Prometheus,队里堆积是最关键的预警信号——堆积意味着消费跟不上生产,离拒绝不远了。

  6. 虚拟线程谨慎试点:JDK 21 后可在纯 IO 接口上试点虚拟线程替代池化,但存量 synchronizedThreadLocal 大量使用的代码需先改造,否则载体线程被钉住反而劣化。

本节相关题目

难度 题目 链接
基础 线程池 7 参数与拒绝策略 → 题库
进阶 P99 劣化如何用线程池+Sentinel 定位治理 → 题库
深度 设计万级 QPS 大促交易链路 → 题库