1. 项目背景
某电商订单处理系统上线三个月后,运维团队发现一个诡异的规律:每逢促销活动流量高峰期,订单服务就会在运行30-45分钟后突然崩溃,服务器直接OOM Kill。排查日志后发现问题根源竟是一行看似无害的代码——Executors.newCachedThreadPool()。
newCachedThreadPool底层使用SynchronousQueue,这意味着每个提交的任务如果没有空闲线程,就会立即创建新线程。在平峰期,每秒几十个订单请求相安无事;但大促期间瞬时并发飙升到每秒3000+请求,线程数从几十暴涨到数万,每个线程默认1MB栈内存,仅线程栈就消耗了数十GB内存,直接将整个JVM进程推向OOM深渊。
雪上加霜的是,系统还使用了ThreadLocal来传递请求链路追踪上下文(traceId、spanId)。在每次热部署上线后,应用程序类加载器被替换,但线程池中的线程由于是ThreadPoolExecutor创建的"非当前类加载器线程",继续持有旧的ThreadLocal引用。ThreadLocalMap中的Entry继承自WeakReference,Key(ThreadLocal对象)被GC回收后变为null,但Value依然被Entry强引用、Entry被ThreadLocalMap强引用、ThreadLocalMap被Thread强引用——这条强引用链导致Value永远无法被回收。经过三天左右运行,Metaspace中累积了大量旧类加载器无法卸载,最终Metaspace OOM。
这两个问题分别代表了Java业务系统中最常见的两类稳定性"地雷":线程池治理缺失和ThreadLocal泄漏。本章将深入OpenJDK源码,从原理到实战,逐一拆解这两个问题的根源与解决之道。
2. 项目设计
小胖:(挠头)大师,救命!我们订单系统又挂了,运维说线程数飙到3万多,内存直接爆了。我就是简单地用了Executors.newCachedThreadPool(),这不是官方API嘛,怎么就出问题了呢?
大师:(放下手中的茶杯,微微一笑)两个问题。第一,你把阿里Java开发手册当耳边风了?第二,你知道newCachedThreadPool底层是怎么创建线程的吗?
小白:(翻开JDK源码)我看了,newCachedThreadPool返回的是:
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<>());
maximumPoolSize是Integer.MAX_VALUE——相当于无上限创建线程。SynchronousQueue是一个没有容量的阻塞队列,每个put操作必须等待一个take操作。这就意味着,只要没有空闲线程,每个新任务都会触发创建新线程。
大师:没错。线程池的7个核心参数缺一不可,了解它们才能驾驭线程池。标准构造函数如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
线程池的任务调度遵循严格的阶梯流程:当一个任务通过execute()提交时——
阶段一:如果当前运行线程数 < corePoolSize,即使有空闲线程,也会创建新线程来处理任务。这是预热机制。
阶段二:如果运行线程数 >= corePoolSize,任务被加入workQueue排队等待。此阶段不会创建新线程。
阶段三:如果workQueue已满且运行线程数 < maximumPoolSize,创建新的非核心线程处理任务。
阶段四:如果workQueue已满且运行线程数已达到maximumPoolSize,触发拒绝策略RejectedExecutionHandler处理该任务。
这段逻辑在ThreadPoolExecutor.execute()方法中非常清晰,源码位于java.util.concurrent.ThreadPoolExecutor,核心代码如下(简化版):
public void execute(Runnable command) {
if (command == null) throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true)) // 创建核心线程
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) { // 尝试入队
int recheck = ctl.get();
if (!isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false)) // 创建非核心线程
reject(command); // 触发拒绝策略
}
小白:(若有所思)那常见的三种阻塞队列各有什么讲究?
大师:问得好。这恰恰是90%的线程池问题根源所在。
-
SynchronousQueue(CachedThreadPool默认):容量为0,每个插入必须等待一个移除。相当于"没有仓库,货到了必须有工人立即处理,否则就招新工人"。如果任务处理速度跟不上提交速度,线程数会无限增长。适合CPU密集型、快速执行的小任务。
-
LinkedBlockingQueue(FixedThreadPool默认):无界队列(Integer.MAX_VALUE容量)。相当于"仓库无限大,所有货先堆着"。这会导致maximumPoolSize形同虚设——因为队列永远不会满,永远走不到阶段三,线程数最多就是corePoolSize。任务堆积在队列中可能导致延迟雪崩:一个慢任务阻塞队列头,后续所有任务排长队。
-
ArrayBlockingQueue:有界队列,需要明确指定容量。相当于"仓库就500平米,满了再招临时工,招到上限还不行就拒绝"。这是最可控的方案,配合合理的拒绝策略,可以优雅地实施背压(Backpressure)。
小胖:等等,那newFixedThreadPool又有什么坑?它看起来线程数固定,不是挺安全的吗?
大师:(在终端敲了几行代码)
// newFixedThreadPool(10) 等价于:
return new ThreadPoolExecutor(10, 10,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>());
问题出在无界的LinkedBlockingQueue。线程数是固定了,但队列可以无限积压。假设10个线程都在处理耗时任务,新来的第11、12...第100万个任务会全部堆积在队列里,内存逐渐被任务对象吞噬,最终也是OOM。只不过这次不是线程数爆了,而是队列内存爆了。
这就是阿里巴巴Java开发手册强制禁止使用Executors工厂方法的原因——它们隐藏了关键的配置细节,让开发者以为在"简单快速"地使用线程池,实际上埋下了生产级隐患。
小白:那拒绝策略怎么选?四种内置策略够用吗?
大师:四种内置拒绝策略各有适用场景:
| 策略 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy(默认) | 抛出RejectedExecutionException | 需要快速失败、上层感知的场景 |
| CallerRunsPolicy | 由提交任务的线程自己执行 | 实现自然的背压流控,提交线程被占用后提交速度自然下降 |
| DiscardPolicy | 静默丢弃新任务 | 允许丢数据的非关键场景(如日志采样) |
| DiscardOldestPolicy | 丢弃队列头部最旧任务,重试提交 | 优先处理最新数据(如实时行情推送) |
生产级系统强烈推荐自定义拒绝策略:结合监控告警、降级逻辑、持久化到消息队列等多层兜底。
小胖:(突然想到)那线程池关闭呢?我直接kill进程行不行?
大师:(摇头)优雅关闭是线程池治理的最后一公里。
// 标准优雅关闭范式
executor.shutdown(); // 不再接收新任务,但等待已提交任务执行完毕
try {
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow(); // 超时则强制中断
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
// 仍然未终止,记录严重错误
}
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
shutdown()将线程池状态改为SHUTDOWN,拒绝新任务但继续执行队列中的任务;shutdownNow()将状态改为STOP,中断所有工作线程,返回未执行的任务列表。关键区别:shutdownNow()调用了Thread.interrupt(),这只是一种协作式中断——如果任务的代码不检查中断标志(或者阻塞在不可中断的操作上),线程就不会停止。因此所有Runnable/Callable任务都应遵循中断规范。
小白:动态调整线程池大小呢?Set方法线程安全吗?
大师:ThreadPoolExecutor提供了setCorePoolSize()和setMaximumPoolSize()方法,是线程安全的。这意味着可以在运行时通过监控指标动态调整线程池规模。例如,当队列积压超过阈值时,自动调大corePoolSize;当系统负载下降后,再调小。配合keepAliveTime参数,非核心线程空闲后会自动回收。
大师:(话锋一转)线程池告一段落,现在我们聊聊ThreadLocal这个"隐式参数传递"利器的另一面——内存泄漏。
小胖:(惊讶)ThreadLocal也会内存泄漏?它不是用WeakReference了吗?
大师:这正是最容易产生误解的地方。让我们深入源码。
每个Thread对象内部维护一个ThreadLocal.ThreadLocalMap threadLocals字段。ThreadLocalMap是一个自定义的哈希表,其内部Entry继承自WeakReference<ThreadLocal<?>>:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; // 强引用!
Entry(ThreadLocal<?> k, Object v) {
super(k); // Key是弱引用
this.value = v; // Value是强引用
}
}
当ThreadLocal对象本身不再被外部强引用时,垃圾回收器会回收Key(因为Entry对它的引用是弱引用),Key变为null。但问题来了:Value依然被Entry强引用,Entry被ThreadLocalMap强引用,ThreadLocalMap被Thread强引用。只要线程还活着(线程池中的线程通常长期存活),这条引用链就不会断开,Value就永远不会被GC回收。这就是ThreadLocal泄漏的根本原因。
看一个具体的泄漏链路:
Thread → threadLocals(ThreadLocalMap) → table[](Entry[]) → Entry → value (强引用,不可达)
↓
key = null (弱引用,已被GC回收)
更可怕的是,在Web应用中如果使用了热部署(如Spring DevTools),每部署一次就创建一个新的类加载器。线程池中的线程持有着旧类加载器加载的类的ThreadLocal,导致整个旧类加载器和它加载的所有类都无法被卸载,Metaspace不断膨胀,最终Metaspace OOM。
延伸话题:InheritableThreadLocal可以在创建子线程时,将父线程的ThreadLocal值复制到子线程。这在请求链路追踪中非常有用——主线程的traceId能自动传递到异步线程:
InheritableThreadLocal<String> traceContext = new InheritableThreadLocal<>();
// 父线程设置值后,fork出的子线程自动继承
但需要注意:
- 线程池中线程是复用的,子线程创建只发生一次,后续不会自动更新InheritableThreadLocal的值。解决方案是使用阿里巴巴开源的
TransmittableThreadLocal(TTL)。 - 虚拟线程(Virtual Thread,JDK 21+)也支持ThreadLocal,但每个虚拟线程的ThreadLocal堆积同样会造成内存问题,且大量虚拟线程会放大泄漏影响。建议在虚拟线程中谨慎使用ThreadLocal,或使用JDK 21引入的
ScopedValue(预览特性)作为替代。ScopedValue提供不可变的作用域绑定,天然无泄漏风险。
技术映射(全章汇总)
| 生活比喻 | 技术概念 | 源码位置 |
|---|---|---|
| 常驻工人 vs 临时工 vs 缓冲仓库 | corePoolSize / maximumPoolSize / workQueue 三角关系(生产者-消费者模式) | ThreadPoolExecutor.execute() |
| 仓库无限大,货先堆着,不知仓库何时爆 | LinkedBlockingQueue无界导致FixedThreadPool队列内存OOM | Executors.newFixedThreadPool() |
| 没有仓库,货到必须立即处理,否则无限招新人 | SynchronousQueue零容量导致CachedThreadPool无限创建线程 | Executors.newCachedThreadPool() |
| 仓库500平米,满了招临时工,招满就拒单 | ArrayBlockingQueue有界队列+合理拒绝策略=可控背压 | ArrayBlockingQueue |
| 延迟 vs 拒单:宁愿排队等还是直接拒 | 队列类型选择的权衡:LinkedBlockingQueue偏好延迟、SynchronousQueue偏好拒绝、ArrayBlockingQueue折中 | 三种 BlockingQueue 实现 |
| 核心线程数 + 队列容量 + 最大线程数 = 承载上限 | 线程池治理核心公式:任一维度无界都导致资源泄漏 | ThreadPoolExecutor 构造函数 |
| Key是弱绳(一拽就断)、Value是铁链(永远不断) | ThreadLocal泄漏根因:WeakReference Key被GC回收后Value强引用链不断 | ThreadLocal.ThreadLocalMap.Entry |
| 仓库钥匙(旧类加载器)被铁链拴住,永远无法丢弃 | 热部署场景:Thread→ThreadLocalMap→Entry→value→旧Class→旧ClassLoader 造成Metaspace OOM | Thread.threadLocals |
3. 项目实战
3.1 环境准备
| 环境组件 | 版本/工具 | 用途 |
|---|---|---|
| JDK | 21 LTS | 运行Java代码,使用虚拟线程支持 |
| 构建工具 | Maven 3.9+ | 项目依赖管理 |
| JMH | 1.37 | 微基准测试,验证线程池性能 |
| VisualVM | 2.1.9 | JVM运行时监控,观察线程状态和堆内存 |
| JConsole | JDK内置 | MBean监控,查看线程池指标 |
| MAT (Memory Analyzer) | 1.15.0 | 堆转储分析,定位ThreadLocal泄漏 |
| Maven依赖 | JMH Core, JMH Generator AnnProcess | Benchmark框架 |
pom.xml关键依赖:
<dependencies>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.37</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.37</version>
<scope>provided</scope>
</dependency>
</dependencies>
3.2 分步实现
步骤一:复现线程池堆积与拒绝策略
本步骤的目标是构建一个有界线程池,模拟高负载场景下的任务堆积和拒绝过程。完整可运行代码如下:
package com.column.chapter19;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 线程池堆积与拒绝策略复现
* 核心线程2个,最大线程4个,队列容量10
* 快速提交20个长时间任务,观察:
* - 前2个任务直接由核心线程处理
* - 3~12任务进入队列
* - 13~14任务创建新线程(临时工)
* - 15~20任务触发拒绝策略
*/
public class ThreadPoolOverflowDemo {
public static void main(String[] args) {
AtomicInteger taskCounter = new AtomicInteger(1);
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // corePoolSize
4, // maximumPoolSize
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new ArrayBlockingQueue<>(10), // 有界队列,容量10
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "order-worker-" + taskCounter.getAndIncrement());
System.out.println("[ThreadFactory] 创建线程: " + t.getName());
return t;
}
},
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.err.println("[拒绝策略] 任务被拒绝!当前线程数="
+ executor.getPoolSize()
+ ", 活跃线程=" + executor.getActiveCount()
+ ", 队列大小=" + executor.getQueue().size());
}
}
);
// 提交20个任务,每个任务执行10秒
for (int i = 1; i <= 20; i++) {
final int taskId = i;
try {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName()
+ " 开始执行任务-" + taskId
+ " [时间=" + System.currentTimeMillis() % 100000 + "]");
try {
Thread.sleep(10_000); // 模拟耗时业务处理
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(Thread.currentThread().getName()
+ " 完成任务-" + taskId);
});
System.out.println("提交任务-" + i + " 成功,当前队列大小: "
+ executor.getQueue().size());
} catch (RejectedExecutionException e) {
System.err.println("提交任务-" + i + " 失败: " + e.getMessage());
}
try {
Thread.sleep(100); // 控制提交速度
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 打印最终状态
System.out.println("\n===== 线程池最终状态 =====");
System.out.println("核心线程数: " + executor.getCorePoolSize());
System.out.println("最大线程数: " + executor.getMaximumPoolSize());
System.out.println("当前池大小: " + executor.getPoolSize());
System.out.println("活跃线程数: " + executor.getActiveCount());
System.out.println("队列剩余容量: " + executor.getQueue().remainingCapacity());
System.out.println("已完成任务数: " + executor.getCompletedTaskCount());
executor.shutdown();
}
}
运行输出分析:前两个任务立即创建核心线程处理(共2个线程),接下来的10个任务被加入队列等待,第13-14个任务触发创建非核心线程(线程总数达到最大值4),第15-20个任务触发拒绝策略。这正是execute()方法中经典的四阶段调度流程的可视化展示。
关键源码验证:在ThreadPoolExecutor.execute()中(OpenJDK源码),addWorker(command, true)表示创建核心线程,addWorker(command, false)表示创建非核心线程。addWorker内部的CAS自旋retry:标签循环是JUC中经典的乐观锁实现模式。
步骤二:实现优雅关闭与中断处理
本步骤演示两种关闭方式的区别,以及任务如何正确响应中断信号:
package com.column.chapter19;
import java.util.List;
import java.util.concurrent.*;
/**
* 线程池优雅关闭对比演示
*
* 场景1:shutdown() - 平稳着陆
* 场景2:shutdownNow() - 紧急迫降
* 场景3:混合模式 - 先礼貌请求,超时再强制
*/
public class GracefulShutdownDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("========== 场景1: shutdown() 优雅关闭 ==========");
demoShutdown();
System.out.println("\n========== 场景2: shutdownNow() 中断关闭 ==========");
demoShutdownNow();
System.out.println("\n========== 场景3: 分层关闭(推荐模式) ==========");
demoLayeredShutdown();
}
/**
* shutdown(): 拒绝新任务,但等待已提交的任务(包括队列中的)正常执行完毕。
* 线程池状态变为SHUTDOWN。
*/
private static void demoShutdown() throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10));
// 提交5个长时间任务
for (int i = 1; i <= 5; i++) {
final int id = i;
executor.execute(() -> {
System.out.println(" 任务-" + id + " 开始执行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(" 任务-" + id + " 被中断!");
Thread.currentThread().interrupt();
return;
}
System.out.println(" 任务-" + id + " 正常完成");
});
}
Thread.sleep(500); // 等待部分任务启动
executor.shutdown();
System.out.println(" 已调用shutdown(),不再接受新任务");
boolean terminated = executor.awaitTermination(10, TimeUnit.SECONDS);
System.out.println(" 线程池是否完全终止: " + terminated);
}
/**
* shutdownNow(): 立即中断所有工作线程,返回未执行的任务列表。
* 线程池状态变为STOP。
* 注意:如果任务不检查中断标志或阻塞在不可中断操作上,线程不会停止!
*/
private static void demoShutdownNow() throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10));
for (int i = 1; i <= 5; i++) {
final int id = i;
executor.execute(() -> {
System.out.println(" 任务-" + id + " 开始执行");
// 关键:任务代码必须响应中断
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(1000);
System.out.println(" 任务-" + id + " 仍在运行...");
} catch (InterruptedException e) {
System.out.println(" 任务-" + id + " 捕获InterruptedException!");
Thread.currentThread().interrupt(); // 恢复中断状态
break;
}
}
System.out.println(" 任务-" + id + " 响应中断退出");
});
}
Thread.sleep(500);
List<Runnable> abandoned = executor.shutdownNow();
System.out.println(" 未执行的任务数: " + abandoned.size());
System.out.println(" 注意:正在执行中的任务不会被返回,只有队列中等待的任务会返回");
executor.awaitTermination(10, TimeUnit.SECONDS);
}
/**
* 生产级分层关闭范式:
* 1. shutdown():礼貌请求停止
* 2. awaitTermination(30s):给30秒时间完成
* 3. shutdownNow():超时则强制中断
* 4. awaitTermination(10s):再给10秒处理中断
* 5. 仍未终止:记录严重告警,可能需要外部kill
*/
private static void demoLayeredShutdown() throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10));
executor.execute(() -> {
// 模拟一个不响应中断的顽固任务
for (int i = 0; i < 100; i++) {
// 不检查中断标志,也不抛出InterruptedException
heavyComputation();
}
});
Thread.sleep(200);
System.out.println(" 开始分层关闭...");
// 第一层:礼貌请求
executor.shutdown();
System.out.println(" 第1层: shutdown()已调用");
// 第二层:等待30秒
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
System.out.println(" 第2层: 30秒超时,调用shutdownNow()");
// 第三层:强制中断
List<Runnable> remaining = executor.shutdownNow();
System.out.println(" 第3层: shutdownNow()已调用,返回" + remaining.size() + "个未执行任务");
// 第四层:再等待10秒
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
System.err.println(" 第4层: 线程池仍然未终止!建议发送P0告警");
} else {
System.out.println(" 第4层: 线程池最终终止");
}
} else {
System.out.println(" 第2层: 线程池在30秒内正常终止");
}
}
private static void heavyComputation() {
// 模拟不响应中断的纯CPU运算
long sum = 0;
for (long i = 0; i < 10_000_000; i++) {
sum = (sum + i) % Integer.MAX_VALUE;
}
}
}
运行此示例可以清晰地看到三种关闭模式的行为差异。特别需要关注的是,Thread.interrupt()只能中断处于WAITING/TIMED_WAITING状态的线程(如sleep、wait、join),对于纯CPU计算或阻塞在不可中断I/O上的线程(如Socket.read()),interrupt()无法直接终止。这是理解线程中断机制的关键。
源码分析:shutdownNow()在interruptWorkers()中遍历workers集合调用Thread.interrupt()。而awaitTermination()内部使用自旋+Condition.awaitNanos()实现对终止状态的等待,这是ReadWriteLock和AQS中常见的条件等待模式。
步骤三:动态线程池监控与JMX集成
构建一个可观测的线程池监控组件,将线程池指标暴露给外部监控系统:
package com.column.chapter19;
import javax.management.*;
import java.lang.management.ManagementFactory;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
/**
* 可监控线程池包装器
*
* 参考美团动态线程池的设计思路,核心是在线程池执行前后添加钩子,
* 收集任务执行时间、排队时间等关键指标。
*
* 与标准ThreadPoolExecutor扩展点的关系:
* - beforeExecute/afterExecute:钩子方法,用于收集每个任务的执行耗时
* - terminated():线程池终止时的回调
* - getTask():每次从队列取任务时更新排队等待时间
*/
public class MonitoredThreadPool extends ThreadPoolExecutor
implements MonitoredThreadPoolMBean {
private final String poolName;
private final LongAdder totalExecutionTime = new LongAdder();
private final AtomicLong maxExecutionTime = new AtomicLong(0);
private final AtomicLong maxQueueSize = new AtomicLong(0);
// 每个线程关联的任务提交时间戳
private final ConcurrentHashMap<Thread, Long> submitTimeMap = new ConcurrentHashMap<>();
public MonitoredThreadPool(String poolName,
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
this.poolName = poolName;
registerMBean();
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
// 记录任务开始执行的时间
submitTimeMap.put(t, System.currentTimeMillis());
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
try {
Long startTime = submitTimeMap.remove(Thread.currentThread());
if (startTime != null) {
long executionTime = System.currentTimeMillis() - startTime;
totalExecutionTime.add(executionTime);
// CAS更新最大执行时间
long currentMax;
do {
currentMax = maxExecutionTime.get();
if (executionTime <= currentMax) break;
} while (!maxExecutionTime.compareAndSet(currentMax, executionTime));
}
} finally {
// 更新最大队列大小
int qSize = getQueue().size();
long currentMax;
do {
currentMax = maxQueueSize.get();
if (qSize <= currentMax) break;
} while (!maxQueueSize.compareAndSet(currentMax, qSize));
}
}
@Override
protected void terminated() {
super.terminated();
unregisterMBean();
System.out.println("[Monitor] 线程池[" + poolName + "] 已终止");
}
// ========== MBean接口实现 ==========
@Override
public String getPoolName() {
return poolName;
}
@Override
public int getActiveThreadCount() {
return getActiveCount();
}
@Override
public int getCurrentPoolSize() {
return getPoolSize();
}
@Override
public int getQueueSize() {
return getQueue().size();
}
@Override
public int getQueueRemainingCapacity() {
return getQueue().remainingCapacity();
}
@Override
public long getCompletedTaskCount() {
return super.getCompletedTaskCount();
}
@Override
public long getTotalTaskCount() {
return getTaskCount();
}
@Override
public double getAverageExecutionTimeMs() {
long completed = getCompletedTaskCount();
return completed > 0
? (double) totalExecutionTime.sum() / completed
: 0.0;
}
@Override
public long getMaxExecutionTimeMs() {
return maxExecutionTime.get();
}
@Override
public int getHistoricalMaxQueueSize() {
return (int) maxQueueSize.get();
}
@Override
public double getTaskRejectionRate() {
// rejectionCount可以通过重写RejectedExecutionHandler来统计
return 0.0; // 简化实现
}
// ========== JMX注册与注销 ==========
private void registerMBean() {
try {
ObjectName objectName = new ObjectName(
"com.column.chapter19:type=MonitoredThreadPool,name=" + poolName);
ManagementFactory.getPlatformMBeanServer()
.registerMBean(this, objectName);
System.out.println("[JMX] MBean注册成功: " + objectName);
} catch (Exception e) {
System.err.println("[JMX] MBean注册失败: " + e.getMessage());
}
}
private void unregisterMBean() {
try {
ObjectName objectName = new ObjectName(
"com.column.chapter19:type=MonitoredThreadPool,name=" + poolName);
ManagementFactory.getPlatformMBeanServer()
.unregisterMBean(objectName);
} catch (Exception e) {
// 静默处理
}
}
}
/**
* MBean接口定义,暴露给JMX监控系统
* 可通过JConsole/VisualVM直接查看
*/
interface MonitoredThreadPoolMBean {
String getPoolName();
int getActiveThreadCount();
int getCurrentPoolSize();
int getQueueSize();
int getQueueRemainingCapacity();
long getCompletedTaskCount();
long getTotalTaskCount();
double getAverageExecutionTimeMs();
long getMaxExecutionTimeMs();
int getHistoricalMaxQueueSize();
double getTaskRejectionRate();
}
/**
* 监控演示主程序
*/
class MonitorDemo {
public static void main(String[] args) throws Exception {
MonitoredThreadPool pool = new MonitoredThreadPool(
"order-processor",
2, // corePoolSize
5, // maximumPoolSize
30, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(20));
System.out.println("JMX MBean已注册,请使用JConsole连接查看指标");
System.out.println("JConsole连接: localhost:" + ProcessHandle.current().pid());
System.out.println("MBean路径: com.column.chapter19 > MonitoredThreadPool > order-processor\n");
// 模拟提交任务
for (int i = 1; i <= 30; i++) {
final int taskId = i;
try {
pool.execute(() -> {
try {
Thread.sleep(500 + (long)(Math.random() * 1000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
} catch (RejectedExecutionException e) {
System.out.println("任务-" + taskId + " 被拒绝,队列已满");
}
Thread.sleep(100);
}
// 定期输出监控快照
ScheduledExecutorService reporter = Executors.newSingleThreadScheduledExecutor();
reporter.scheduleAtFixedRate(() -> {
System.out.println("\n===== 线程池快照 =====");
System.out.println("活跃线程: " + pool.getActiveThreadCount());
System.out.println("当前池大小: " + pool.getCurrentPoolSize());
System.out.println("队列大小: " + pool.getQueueSize());
System.out.println("已完成任务: " + pool.getCompletedTaskCount());
System.out.println("平均耗时ms: " + String.format("%.2f", pool.getAverageExecutionTimeMs()));
System.out.println("历史最大队列: " + pool.getHistoricalMaxQueueSize());
}, 500, 2000, TimeUnit.MILLISECONDS);
Thread.sleep(15_000);
reporter.shutdown();
pool.shutdown();
}
}
与Spring Boot Actuator集成思路:在Spring Boot 2.x+中,可以通过注册自定义MeterBinder将线程池指标接入Micrometer,进而输出到Prometheus + Grafana。美团技术团队的动态线程池实践在此基础上增加了配置中心(如Apollo、Nacos)的实时下发能力,实现了线程池参数的运行时动态调整。
动态调整线程池示例:
// 通过set方法在运行时调整线程池参数
pool.setCorePoolSize(8); // 从2调到8
pool.setMaximumPoolSize(20); // 从5调到20
pool.setKeepAliveTime(120, TimeUnit.SECONDS); // 调整空闲存活时间
// 允许核心线程超时回收(JDK 1.6+)
pool.allowCoreThreadTimeOut(true);
// 预热所有核心线程(JDK 1.6+)
pool.prestartAllCoreThreads();
步骤四:ThreadLocal泄漏复现与修复
本步骤通过构造一个真实的内存泄漏场景,使用jcmd和MAT工具进行堆转储分析:
package com.column.chapter19;
import java.lang.reflect.Field;
import java.util.concurrent.*;
/**
* ThreadLocal内存泄漏复现与修复演示
*
* 工作流程:
* 1. 创建固定大小的线程池
* 2. 提交任务,每个任务向ThreadLocal中存入一个大的对象(模拟业务上下文)
* 3. 任务结束后不清除ThreadLocal(复现泄漏)
* 4. 强制GC,观察堆中仍然保留着ThreadLocal的Value
*
* 堆转储分析命令(程序运行时在另一个终端执行):
* jcmd <pid> GC.heap_dump /tmp/heap_before_remove.hprof
* jcmd <pid> GC.heap_dump /tmp/heap_after_remove.hprof
*
* MAT分析步骤:
* File -> Open Heap Dump -> Open Query Browser -> Java Basics -> Thread Details
* 展开线程 -> 查看 threadLocals -> 查看 table[] -> 确认 Entry[] 中的 value
*/
public class ThreadLocalLeakDemo {
/**
* 模拟业务上下文,包含大量数据
*/
static class BizContext {
private final byte[] payload; // 模拟大对象,实际业务中可能是用户信息、权限等
private final String traceId;
private final String userId;
public BizContext(String traceId, String userId) {
this.traceId = traceId;
this.userId = userId;
// 分配1MB大小模拟大对象
this.payload = new byte[1024 * 1024];
}
@Override
public String toString() {
return "BizContext{traceId='" + traceId + "', userId='" + userId + "'}";
}
}
// ThreadLocal定义:通常放在静态字段或工具类中
private static final ThreadLocal<BizContext> CONTEXT_HOLDER = new ThreadLocal<>();
public static void main(String[] args) throws Exception {
System.out.println(">>> 程序PID: " + ProcessHandle.current().pid());
System.out.println(">>> 使用命令: jcmd " + ProcessHandle.current().pid()
+ " GC.heap_dump heap_leak.hprof");
System.out.println();
// ===== 第一阶段:泄漏复现 =====
System.out.println("========== 第一阶段:泄漏复现 ==========");
ExecutorService pool = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 10; i++) {
final int idx = i;
pool.execute(() -> {
// 每个任务设置自己的ThreadLocal值
CONTEXT_HOLDER.set(new BizContext("trace-" + idx, "user-" + idx));
System.out.println(Thread.currentThread().getName()
+ " 设置ThreadLocal: " + CONTEXT_HOLDER.get());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// ★★★ 关键Bug:没有在finally中调用remove() ★★★
// CONTEXT_HOLDER.remove();
});
}
System.out.println(">>> 等待任务完成...");
Thread.sleep(3000);
// 引用链路检查:如果ThreadLocal内的值还存活,说明泄漏了
// 每个线程最后设置的BizContext对象及其1MB payload仍被引用
System.out.println(">>> 强制触发GC...");
System.gc();
Thread.sleep(2000);
System.out.println(">>> 请使用jcmd + MAT分析堆转储文件,查找BizContext实例");
System.out.println(">>> 预期:每个线程池线程仍持有1个BizContext对象(~1MB each)\n");
// 使用反射检查ThreadLocalMap内部状态(仅供演示,勿在生产中使用)
inspectThreadLocals();
// ===== 第二阶段:正确清理模式 =====
System.out.println("\n========== 第二阶段:正确清理模式 ==========");
ThreadLocal<BizContext> safeHolder = new ThreadLocal<>();
for (int i = 11; i <= 15; i++) {
final int idx = i;
pool.execute(() -> {
try {
safeHolder.set(new BizContext("trace-safe-" + idx, "user-" + idx));
System.out.println(Thread.currentThread().getName()
+ " 设置safeHolder: " + safeHolder.get());
// 执行业务逻辑...
Thread.sleep(300);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// ★★★ 正确方式:在finally中调用remove() ★★★
safeHolder.remove();
}
});
}
Thread.sleep(2000);
System.gc();
Thread.sleep(2000);
System.out.println(">>> 正确清理后,请再次导出堆转储对比BizContext实例数量");
System.out.println(">>> 预期:BizContext实例应该已被GC回收");
pool.shutdown();
}
/**
* 通过反射检查线程的ThreadLocalMap(仅供调试学习)
*
* 源码路径:
* - java.lang.Thread.threadLocals 字段
* - java.lang.ThreadLocal.ThreadLocalMap.table 字段
* - java.lang.ThreadLocal.ThreadLocalMap.Entry.value 字段
*/
@SuppressWarnings("unchecked")
private static void inspectThreadLocals() throws Exception {
Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
threadLocalsField.setAccessible(true);
for (Thread t : Thread.getAllStackTraces().keySet()) {
Object threadLocals = threadLocalsField.get(t);
if (threadLocals == null) continue;
Field tableField = threadLocals.getClass().getDeclaredField("table");
tableField.setAccessible(true);
Object[] table = (Object[]) tableField.get(threadLocals);
int entryCount = 0;
int leakedCount = 0;
for (Object entry : table) {
if (entry == null) continue;
entryCount++;
// 检查Entry中的Key是否为null(泄漏标志)
Class<?> entryClass = entry.getClass();
Field referentField = entryClass.getSuperclass().getDeclaredField("referent");
referentField.setAccessible(true);
Object key = referentField.get(entry);
if (key == null) {
leakedCount++;
}
}
if (entryCount > 0) {
System.out.println("线程[" + t.getName() + "]: ThreadLocalMap条目数="
+ entryCount + ", 泄漏条目(key=null)=" + leakedCount);
}
}
}
}
泄漏验证步骤:
# 终端1:运行程序
java -Xmx128m com.column.chapter19.ThreadLocalLeakDemo
# 终端2:在提示点导出堆转储
jcmd <pid> GC.heap_dump heap_before_cleanup.hprof
# 使用MAT分析
# 1. Open heap_before_cleanup.hprof
# 2. Open Query Browser → Java Basics → Thread Details
# 3. 找到 "pool-1-thread-1",展开 "threadLocals"
# 4. 查看 "table" 数组中的 Entry → 确认 value 指向 BizContext
# 5. Open Dominator Tree → 搜索 BizContext → 查看 Retained Heap 大小
InheritableThreadLocal的陷阱与TTL解决方案:
package com.column.chapter19;
import java.util.concurrent.*;
/**
* InheritableThreadLocal在父子线程传递中的失效场景
*
* 原理:在创建新线程时,Thread.init()会调用
* ThreadLocal.createInheritedMap()将父线程的inheritableThreadLocals
* 浅拷贝到子线程。但线程池复用线程,子线程只创建一次,
* 后续父线程的更新无法传递。
*
* 源码位置:java.lang.Thread.init() → createInheritedMap()
*/
public class InheritableThreadLocalTrap {
private static final InheritableThreadLocal<String> INHERITABLE_CONTEXT
= new InheritableThreadLocal<>();
private static final ThreadLocal<String> NORMAL_CONTEXT
= new ThreadLocal<>();
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(2);
// ===== 场景1:正常ThreadLocal不会传递到子线程 =====
NORMAL_CONTEXT.set("parent-value");
pool.execute(() -> {
String childValue = NORMAL_CONTEXT.get();
System.out.println("子线程中Normal ThreadLocal值: "
+ childValue + " (预期: null, 因为不传递)");
});
Thread.sleep(500);
// ===== 场景2:InheritableThreadLocal在首次创建线程时传递 =====
INHERITABLE_CONTEXT.set("first-context");
CompletableFuture<Void> f1 = CompletableFuture.runAsync(() -> {
System.out.println("子线程-首次 InheritableTL值: "
+ INHERITABLE_CONTEXT.get() + " (预期: first-context)");
}, pool);
f1.join();
// ===== 场景3:线程复用后,父线程更新但子线程获取的是旧值 =====
INHERITABLE_CONTEXT.set("second-context");
CompletableFuture<Void> f2 = CompletableFuture.runAsync(() -> {
System.out.println("子线程-复用后 InheritableTL值: "
+ INHERITABLE_CONTEXT.get()
+ " (预期: first-context而非second-context, 线程复用时不会重新复制!)");
}, pool);
f2.join();
// ===== 解决方案 =====
// 方案A:在每个任务提交前手动传递上下文
// 方案B:使用阿里巴巴TransmittableThreadLocal
// <dependency>
// <groupId>com.alibaba</groupId>
// <artifactId>transmittable-thread-local</artifactId>
// <version>2.14.4</version>
// </dependency>
//
// TransmittableThreadLocal<String> ttlContext = new TransmittableThreadLocal<>();
// ExecutorService ttlPool = TtlExecutors.getTtlExecutorService(pool);
// 此后,父线程对ttlContext的修改能自动传递到已复用的子线程
System.out.println("\n解决方案:使用TransmittableThreadLocal或每次提交时显式传递");
pool.shutdown();
}
}
JDK 21虚拟线程与ThreadLocal的注意事项:
虚拟线程(Virtual Thread)是JDK 21的正式特性。虚拟线程同样支持ThreadLocal,但需要注意:
// 虚拟线程示例
Thread.startVirtualThread(() -> {
ThreadLocal<String> local = new ThreadLocal<>();
local.set("virtual-thread-data");
// your business logic
});
每个虚拟线程都有自己的ThreadLocalMap。大量短生命周期的虚拟线程如果使用ThreadLocal且未及时remove,会造成大量对象堆积。JDK 21引入了ScopedValue(孵化器阶段)作为ThreadLocal的替代方案,其特点是:
- 不可变绑定(immutable binding)
- 有明确的作用域生命周期
- 线程继承时自动共享,无泄漏风险
// ScopedValue示例 (JDK 21+ 需启用 --enable-preview)
// ScopedValue<String> TRACE_ID = ScopedValue.newInstance();
// ScopedValue.where(TRACE_ID, "trace-123").run(() -> {
// String id = TRACE_ID.get(); // "trace-123"
// });
步骤五:自定义拒绝策略——告警+降级+重试
生产环境不能简单地丢弃任务或抛异常,需要构建多层兜底的拒绝策略:
package com.column.chapter19;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
/**
* 生产级自定义拒绝策略
*
* 五层兜底机制:
* 1. 记录详细拒绝日志(包含堆栈、池状态)
* 2. 尝试重新入队(指数退避重试)
* 3. 降级到备用线程池执行
* 4. 持久化到本地文件/消息队列(异步恢复)
* 5. 触发告警通知(邮件/钉钉/PagerDuty)
*
* 设计参考:Resilience4j的Bulkhead模式 + CircuitBreaker的降级理念
*/
public class ProductionRejectedHandler implements RejectedExecutionHandler {
private static final DateTimeFormatter DTF =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
private final String poolName;
private final ExecutorService fallbackExecutor; // 备用线程池
private final Consumer<String> alertNotifier; // 告警通知回调
private final AtomicLong rejectionCounter = new AtomicLong(0);
public ProductionRejectedHandler(String poolName,
ExecutorService fallbackExecutor,
Consumer<String> alertNotifier) {
this.poolName = poolName;
this.fallbackExecutor = fallbackExecutor;
this.alertNotifier = alertNotifier;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
long count = rejectionCounter.incrementAndGet();
String timestamp = DTF.format(LocalDateTime.now());
// 第1层:详细日志记录
String logMsg = String.format(
"[REJECTED] pool=%s | time=%s | rejection#%d | "
+ "active=%d | poolSize=%d | core=%d | max=%d | "
+ "queueSize=%d | queueRemaining=%d | completed=%d | "
+ "thread=%s | task=%s",
poolName, timestamp, count,
executor.getActiveCount(),
executor.getPoolSize(),
executor.getCorePoolSize(),
executor.getMaximumPoolSize(),
executor.getQueue().size(),
executor.getQueue().remainingCapacity(),
executor.getCompletedTaskCount(),
Thread.currentThread().getName(),
r.toString()
);
System.err.println(logMsg);
// 第2层:指数退避重试(最多3次,间隔100ms/200ms/400ms)
boolean retrySuccess = false;
for (int i = 0; i < 3; i++) {
try {
long backoffMs = 100L * (1 << i); // 100, 200, 400
Thread.sleep(backoffMs);
executor.getQueue().put(r); // 阻塞式放入队列
retrySuccess = true;
System.out.println("[RETRY-SUCCESS] pool=" + poolName
+ " 重试#" + (i + 1) + " 成功,退避=" + backoffMs + "ms");
break;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (RejectedExecutionException e) {
// 重试失败,继续下一轮
}
}
// 第3层:降级到备用线程池
if (!retrySuccess) {
try {
fallbackExecutor.execute(r);
System.out.println("[FALLBACK] pool=" + poolName
+ " 任务降级到备用线程池执行");
retrySuccess = true;
} catch (RejectedExecutionException e) {
System.err.println("[FALLBACK-FAILED] pool=" + poolName
+ " 备用线程池也拒绝!");
}
}
// 第4层:持久化到消息队列(示例用文件替代)
if (!retrySuccess) {
persistToFile(logMsg, r);
}
// 第5层:触发告警
if (count % 100 == 0 || !retrySuccess) { // 每100次拒绝或最终失败时告警
alertNotifier.accept(logMsg);
}
}
/**
* 持久化任务到文件,后续可通过定时任务扫描恢复
* 生产环境应使用Kafka/RocketMQ等消息中间件
*/
private void persistToFile(String logMsg, Runnable r) {
try {
java.nio.file.Files.writeString(
java.nio.file.Path.of("./rejected_tasks.log"),
logMsg + "\n",
java.nio.file.StandardOpenOption.CREATE,
java.nio.file.StandardOpenOption.APPEND
);
} catch (Exception e) {
System.err.println("持久化失败: " + e.getMessage());
}
}
/**
* 演示自定义拒绝策略的使用
*/
public static void main(String[] args) throws Exception {
// 备用线程池(使用CallerRunsPolicy确保不会再次拒绝)
ExecutorService fallbackPool = new ThreadPoolExecutor(
1, 2, 30, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy() // 兜底策略
);
// 告警回调(生产环境接入钉钉/PagerDuty)
Consumer<String> alertNotifier = msg -> {
System.out.println("🚨 [ALERT] " + msg);
// 实际应为: dingTalkClient.send(message) 或 pagerDuty.trigger(incident)
};
// 创建使用自定义拒绝策略的线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 3, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
new ProductionRejectedHandler("order-pool", fallbackPool, alertNotifier)
);
// 提交超过处理能力的任务
for (int i = 1; i <= 10; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("执行任务-" + taskId);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread.sleep(100);
}
Thread.sleep(10_000);
executor.shutdown();
fallbackPool.shutdown();
}
}
四种内置拒绝策略对比代码:
package com.column.chapter19;
import java.util.concurrent.*;
/**
* 四种内置拒绝策略行为对比
*/
public class RejectionPolicyComparison {
public static void main(String[] args) {
testPolicy("AbortPolicy", new ThreadPoolExecutor.AbortPolicy());
testPolicy("CallerRunsPolicy", new ThreadPoolExecutor.CallerRunsPolicy());
testPolicy("DiscardPolicy", new ThreadPoolExecutor.DiscardPolicy());
testPolicy("DiscardOldestPolicy", new ThreadPoolExecutor.DiscardOldestPolicy());
}
private static void testPolicy(String name, RejectedExecutionHandler policy) {
System.out.println("\n===== 测试: " + name + " =====");
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, 1, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1),
policy
);
try {
// 第1个任务:由核心线程执行
executor.execute(() -> {
System.out.println(" 任务1 开始执行");
sleep(2000);
System.out.println(" 任务1 完成");
});
// 第2个任务:进入队列等待
executor.execute(() -> System.out.println(" 任务2 (队列中) 执行"));
// 第3个任务:队列已满,触发拒绝策略
System.out.print(" 提交任务3 -> ");
executor.execute(() -> System.out.println(" 任务3 执行"));
} catch (RejectedExecutionException e) {
System.out.println("抛出异常: " + e.getClass().getSimpleName());
} catch (Exception e) {
System.out.println("其他异常: " + e.getClass().getSimpleName());
}
executor.shutdown();
try {
executor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private static void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
CallerRunsPolicy的背压机制深入分析:
CallerRunsPolicy是最容易被误解的拒绝策略。它看似简单——让提交任务的线程自己执行任务——实际上实现了一种天然的流量控制(背压)机制。当线程池满负荷时,提交线程(通常是Tomcat的Worker线程或Netty的EventLoop线程)被占用去执行任务,自然就无法继续提交新任务,从而降低系统整体压力。但这种策略也有风险:如果提交线程是关键的I/O线程(如Netty的EventLoop),阻塞执行长时间任务会导致整个I/O事件循环停滞。因此在Reactor模型中,CallerRunsPolicy需要谨慎使用。
3.3 测试验证
线程池指标验证表
| 测试场景 | 输入条件 | 预期行为 | 验证方法 | 通过标准 |
|---|---|---|---|---|
| 任务调度阶梯流程 | core=2, max=4, queue=10, 提交20个任务 | 前2直接执行,后续10入队,再2创建线程,剩余6被拒绝 | 观察线程数和队列大小变化 | 各阶段线程数和队列大小完全符合四阶段流程 |
| shutdown优雅关闭 | 提交5个长时间任务后调用shutdown() | 不再接新任务,等待已提交任务全部完成 | awaitTermination返回true | 所有5个任务正常完成,队列清空 |
| shutdownNow中断关闭 | 提交5个sleep任务后调用shutdownNow() | 立即中断线程,返回队列中等待的任务 | 检查返回的未执行任务列表 | 执行中任务收到InterruptedException |
| 拒绝策略-AbortPolicy | 提交任务超过core+queue容量 | 抛出RejectedExecutionException | 捕获异常 | 异常信息包含拒绝原因 |
| 拒绝策略-CallerRunsPolicy | 主线程提交超量任务 | 主线程被占用执行任务 | 检查执行线程名 | 任务在main线程中执行 |
| ThreadLocal-MemoryLeak | 10个任务使用ThreadLocal后不remove | 旧ThreadLocal的value保留在ThreadLocalMap中 | MAT分析 | 堆中存在key=null但value强引用的Entry |
| ThreadLocal-CorrectCleanup | 10个任务使用finally remove | GC后value被回收 | MAT分析 | 堆中无泄漏对象 |
| InheritableThreadLocal复用失效 | 线程池复用线程,父线程更新值 | 子线程获取旧值 | 日志输出对比 | 子线程打印的是首次继承的值,非最新值 |
| 动态调整线程池 | 运行时调用setCorePoolSize(8) | 线程数逐渐增加到8 | JMX指标观察 | 池大小在30秒内达到8 |
| JMH性能基准 | 100K任务提交 | 测量吞吐量和延迟P99 | JMH结果输出 | 吞吐量对比和P99延迟在合理范围 |
JMH微基准测试
package com.column.chapter19;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
/**
* JMH基准测试:对比不同线程池配置的吞吐量和延迟
*
* 运行命令:
* java -jar target/benchmarks.jar ThreadPoolBenchmark
*/
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Benchmark)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 2)
@Fork(1)
public class ThreadPoolBenchmark {
@Param({"4", "8", "16"})
private int threadCount;
@Param({"ARRAY", "LINKED", "SYNCHRONOUS"})
private String queueType;
private ThreadPoolExecutor executor;
@Setup(Level.Trial)
public void setup() {
BlockingQueue<Runnable> queue;
switch (queueType) {
case "ARRAY":
queue = new ArrayBlockingQueue<>(1000);
break;
case "LINKED":
queue = new LinkedBlockingQueue<>(1000);
break;
case "SYNCHRONOUS":
queue = new SynchronousQueue<>();
break;
default:
queue = new LinkedBlockingQueue<>(1000);
}
executor = new ThreadPoolExecutor(
threadCount, threadCount * 2,
30, TimeUnit.SECONDS, queue,
new ThreadPoolExecutor.AbortPolicy());
}
@TearDown(Level.Trial)
public void teardown() {
executor.shutdown();
}
@Benchmark
public void submitTasks() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
try {
executor.execute(() -> {
// 模拟轻量计算任务
Blackhole.consumeCPU(100);
});
} catch (RejectedExecutionException ignored) {
}
}
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder()
.include(ThreadPoolBenchmark.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
堆转储分析命令汇总
# 1. 查看JVM进程
jcmd -l
# 2. 导出堆转储(标准方式)
jcmd <pid> GC.heap_dump /tmp/heap_snapshot.hprof
# 3. 导出堆转储(带时间戳)
jcmd <pid> GC.heap_dump /tmp/heap_$(date +%Y%m%d_%H%M%S).hprof
# 4. 查看线程概要(含ThreadLocal信息)
jcmd <pid> Thread.print
# 5. 查看JVM运行参数
jcmd <pid> VM.flags
# 6. 查看类加载统计(定位Metaspace问题)
jcmd <pid> GC.class_stats
# 7. MAT命令行分析(非交互模式)
# ParseHeapDump.sh heap_snapshot.hprof org.eclipse.mat.api:suspects
# ParseHeapDump.sh heap_snapshot.hprof org.eclipse.mat.api:overview
# 8. 查找ThreadLocal泄漏的OQL查询(在MAT中执行)
# SELECT * FROM INSTANCEOF java.util.concurrent.ThreadPoolExecutor
# 展开threadLocals,检查Entry[].value的类型
# 9. 查找所有ThreadLocal.Entry实例
# SELECT * FROM java.lang.ThreadLocal$ThreadLocalMap$Entry
4. 项目总结
4.1 优点与缺点
| 组件 | 优点 | 缺点 | 替代方案 |
|---|---|---|---|
| ThreadPoolExecutor | 线程复用减少创建开销;灵活的参数配置;支持JMX监控;任务调度可预测 | 配置复杂,参数选择高度依赖业务场景;队列选择不当可能引发OOM或延迟雪崩;默认实现不支持任务优先级 | ForkJoinPool(分治任务);虚拟线程(JDK 21+高并发轻量任务);Disruptor(极低延迟场景) |
| ThreadLocal | 隐式参数传递无需修改方法签名;线程封闭保证线程安全;与Spring事务管理器深度集成 | 使用不当极易内存泄漏;线程池场景下需要额外关注清理;增加代码隐式依赖,降低可测试性 | TransmittableThreadLocal(跨线程传递);ScopedValue(JDK 21+不可变上下文);显式参数传递(最安全) |
| CallerRunsPolicy | 天然的背压流控;不丢失任务;实现简单 | 占用调用线程;不适合Reactor模型中的EventLoop线程;可能导致调用链阻塞 | 自定义多层降级策略;消息队列异步缓冲;RateLimiter前置限流 |
| ArrayBlockingQueue | 有界、可控;内存占用可预测;FIFO公平性好 | 需要合理预估容量;容量设小易触发拒绝,设大浪费内存;单锁设计高并发下存在争用 | LinkedBlockingQueue(双锁设计吞吐更高);Disruptor RingBuffer(无锁);SynchronousQueue(零容量直传) |
三种线程池方案的深度对比:
| 特性 | ThreadPoolExecutor | ForkJoinPool | Virtual Thread (JDK 21) |
|---|---|---|---|
| 适用场景 | 通用任务执行、I/O密集型 | 递归分治、CPU密集型 | 高并发I/O密集型(每请求一线程) |
| 线程模型 | 平台线程+任务队列 | 工作窃取+双端队列 | 平台线程承载多个虚拟线程 |
| 内存开销 | 每线程~1MB栈 | 与ThreadPool相似 | 每虚拟线程~几KB(对象形式存储) |
| 阻塞处理 | 阻塞占用平台线程 | 阻塞占用工作线程 | 阻塞释放平台线程(自动yield) |
| 线程局部变量 | 支持ThreadLocal | 支持ThreadLocal | 支持但需注意大量对象堆积 |
| 核心API | execute/submit | fork/join/invoke | Thread.startVirtualThread() |
4.2 适用场景
线程池适用场景(5个典型)
-
Web服务器请求处理:Tomcat/Jetty的请求处理线程池,控制并发请求数避免系统过载。典型配置:core=CPU核数,max=CPU核数*2,队列=1000。
-
异步消息消费:RocketMQ/Kafka消费者线程池,并行消费提高吞吐。建议core=max=分区数,队列使用ArrayBlockingQueue拒绝时告警。
-
批量数据导出:控制并发导出任务数,避免数据库连接池耗尽。典型配置:core=max=数据库连接池大小/2,使用CallerRunsPolicy。
-
定时任务调度:ScheduledThreadPoolExecutor用于延迟执行和周期性任务,替代Timer(单线程,异常会终止调度)。
-
网关限流熔断:API网关使用线程池隔离不同服务的调用,实现舱壁模式(Bulkhead),防止某个慢服务拖垮整个网关。
线程池不适用场景(2个)
-
纯CPU密集型计算:线程切换带来的上下文切换开销反而降低吞吐。应使用ForkJoinPool或直接使用核心数数量的线程池,队列使用SynchronousQueue。
-
极低延迟场景(微秒级):线程池的锁竞争和上下文切换会破坏延迟确定性。应使用Disruptor等无锁框架或协程方案。
ThreadLocal适用场景(5个典型)
-
分布式链路追踪:存储traceId、spanId,贯穿整个请求生命周期,无需在每个方法签名中传递。
-
Spring事务管理:TransactionSynchronizationManager使用ThreadLocal存储当前线程的事务资源和同步状态。
-
数据库读写分离:使用ThreadLocal存储数据源路由key(如"master"/"slave"),在ORM层透明切换。
-
用户会话上下文:存储当前登录用户信息(userId、tenantId、权限列表),避免在业务层重复查询。
-
日期格式化:SimpleDateFormat非线程安全,使用ThreadLocal为每个线程分配独立实例(JDK 8+建议直接使用DateTimeFormatter)。
ThreadLocal不适用场景(2个)
-
高并发虚拟线程场景:每个虚拟线程一个ThreadLocal副本,大量短生命周期虚拟线程导致对象堆积。建议使用ScopedValue。
-
跨线程共享状态:ThreadLocal的设计就是线程封闭,不应该用于跨线程共享可变状态,这是对ThreadLocal语义的滥用。
4.3 注意事项
| 注意事项 | 详细说明 | 影响版本 |
|---|---|---|
| corePoolSize设置陷阱 | core=0时,空闲时会销毁所有线程,有任务时重新创建。高QPS场景下频繁创建/销毁线程反而降低性能。建议至少设置为1 | 所有JDK版本 |
| prestartAllCoreThreads() | 核心线程默认懒加载(有任务才创建),如果希望线程池预热,调用此方法或prestartCoreThread()。Web应用启动时应主动预热 | JDK 1.5+ |
| allowCoreThreadTimeOut(true) | 允许核心线程也因空闲超时而回收。适合弹性伸缩场景,但要注意核心线程回收后latency增加 | JDK 1.6+ |
| ThreadFactory命名规范 | 务必通过自定义ThreadFactory给线程命名(如"order-worker-%d"),否则默认的"pool-N-thread-M"难以排查问题 | 所有JDK版本 |
| setRejectedExecutionHandler动态切换 | 拒绝策略可以在运行时通过set方法动态切换,配合监控指标实现自动降级 | JDK 1.5+ |
| 容器环境注意事项 | Docker容器中,JDK 8u191之前版本使用宿主机CPU核数计算parallelism,导致线程池配置失真。JDK 8u191+使用-XX:ActiveProcessorCount或cgroup限制。K8s中建议设置-XX:ActiveProcessorCount=<limits.cpu> |
JDK 8u191+ |
| ThreadLocal.remove()时机 | 必须在finally块中调用,否则业务异常会跳过remove导致泄漏。最佳实践:使用try-finally模板,或在框架层面(如Spring的RequestContextHolder)统一管理生命周期 | 所有JDK版本 |
| ThreadLocal.withInitial() | JDK 8+推荐使用ThreadLocal.withInitial(Supplier)替代匿名子类创建,工厂方法确保每个线程首次get时正确初始化 |
JDK 8+ |
| ThreadLocalMap的哈希冲突 | ThreadLocalMap使用开放地址法(线性探测)解决哈希冲突,Entry数量超过threshold(容量*2/3)时扩容为原来的2倍。大量ThreadLocal会导致散列表膨胀 | 所有JDK版本 |
| FastThreadLocal(Netty) | Netty实现了自己的FastThreadLocal,使用数组索引替代哈希表,存取更快且自动清理。如果项目中已使用Netty,优先使用FastThreadLocal | Netty 4.x+ |
JDK版本演进对比:
| JDK版本 | 线程池相关变化 |
|---|---|
| JDK 5 | ThreadPoolExecutor首次引入,Executors工厂类出现 |
| JDK 6 | 新增allowCoreThreadTimeOut、prestartAllCoreThreads |
| JDK 7 | ForkJoinPool正式引入;ThreadLocal.remove()优化,suppressedExceptions支持 |
| JDK 8 | CompletableFuture默认使用ForkJoinPool.commonPool();ThreadLocal.withInitial() |
| JDK 11 | 部分ThreadLocal实现优化,但API无大变化 |
| JDK 17 | ThreadLocal去除finalize()方法,停止使用finalization机制清理 |
| JDK 19 | 虚拟线程预览,ScopedValue孵化器阶段 |
| JDK 21 | 虚拟线程正式GA,ScopedValue处于第二预览阶段 |
4.4 常见踩坑经验
踩坑一:CachedThreadPool在K8s中OOM Kill
故障现象:某支付网关在K8s集群中运行,Pod配置为2C4G。促销期间QPS从200飙升至5000,Pod在5分钟内连续被OOM Kill,重启后继续崩溃,形成"CrashLoopBackOff"。
根因分析:
- 代码中使用了
Executors.newCachedThreadPool()处理第三方支付回调 - 促销期间回调量暴增,SynchronousQueue无法缓冲,线程数从几十暴涨到8000+
- 每个线程默认栈大小1MB(64位JVM),仅线程栈就消耗8GB+内存
- Pod配置的4G内存被吃光,Linux OOM Killer介入杀死JVM进程
- 线程创建开销(分配栈内存、初始化JNI)进一步拖慢系统,形成恶性循环
修复方案:
// 修复前(致命)
ExecutorService pool = Executors.newCachedThreadPool();
// 修复后(可控)
ThreadPoolExecutor pool = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(), // core=CPU核数
Runtime.getRuntime().availableProcessors() * 4, // max=4倍CPU
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2000), // 有界队列2000
new ThreadFactoryBuilder().setNameFormat("pay-callback-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 背压
);
// 关键:队列满时CallerRunsPolicy让Netty的I/O线程执行任务,
// 自然降低回调接收速度,实现自适应流控
教训:永远不要在生产环境使用Executors工厂方法,除非你完全理解底层实现并承受相应的风险。
踩坑二:ThreadLocal泄漏导致Metaspace OOM
故障现象:某Spring Boot应用每隔2-3天热部署一次(通过Jenkins自动部署),运行72小时后应用响应变慢,最终抛出java.lang.OutOfMemoryError: Metaspace,必须重启恢复。
根因分析:
- 应用使用自定义的
RequestContextFilter,在每个请求开始时设置ThreadLocal存储userInfo - Filter中只在正常流程中调用了remove(),但异常流程(如权限校验失败抛出异常)遗漏了remove()
- 线程池使用的是Tomcat默认线程池(maxThreads=200),线程长期存活
- 每次热部署创建新的类加载器,但线程池中的200个线程仍持有旧类加载器加载的RequestContextFilter类的ThreadLocal引用
- JDK 17移除了ThreadLocal的finalize()方法,失去了最后的"钩子清理"机会
- 旧类加载器和它加载的所有类无法被GC(因为ThreadLocalMap→Entry→value→RequestContextFilter对象→RequestContextFilter.class→旧ClassLoader这条引用链),Metaspace不断累积
修复方案:
// 修复前(有漏洞)
@Component
public class RequestContextFilter extends OncePerRequestFilter {
private static final ThreadLocal<UserInfo> USER_CONTEXT = new ThreadLocal<>();
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
USER_CONTEXT.set(extractUserInfo(request));
chain.doFilter(request, response);
USER_CONTEXT.remove(); // Bug: chain抛出异常时不会执行
}
}
// 修复后(防弹)
@Component
public class RequestContextFilter extends OncePerRequestFilter {
private static final ThreadLocal<UserInfo> USER_CONTEXT
= ThreadLocal.withInitial(() -> null);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
USER_CONTEXT.set(extractUserInfo(request));
try {
chain.doFilter(request, response);
} finally {
USER_CONTEXT.remove(); // 无论异常与否都清理
}
}
}
同时配合JVM参数确保热部署后旧类加载器能被回收:
# 启用类卸载(CMS/G1默认开启,但仍建议显式配置)
-XX:+ClassUnloadingWithConcurrentMark
# 适当增大Metaspace,避免因空间不足提前触发Full GC
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
# 监控类加载和卸载
-XX:+TraceClassLoading -XX:+TraceClassUnloading
教训:ThreadLocal.remove()必须出现在finally块中。使用静态代码扫描工具(如SonarQube规则squid:S5164)检测ThreadLocal未清理问题。
踩坑三:shutdownNow()无法停止阻塞I/O的任务
故障现象:订单服务在滚动发布时,旧实例调用shutdownNow()后,线程池仍有线程未终止,awaitTermination超时,最终进程被force kill,导致正在处理中的订单数据不一致。
根因分析:
- 订单处理线程池(10个线程)中有一个任务在执行Socket.connect()连接第三方支付网关
- 第三方支付网关因网络问题无响应,Socket.connect()默认超时时间为平台相关(Linux约20分钟)
- shutdownNow()调用Thread.interrupt(),但Socket.connect()属于不可中断的I/O阻塞(NIO的SocketChannel.connect()才是可中断的)
- 线程在阻塞I/O上hang住,不响应中断信号
修复方案:
// 方案1:设置Socket连接超时(治本)
Socket socket = new Socket();
socket.connect(new InetSocketAddress(host, port), 5000); // 5秒超时
// 方案2:使用NIO的SocketChannel(可中断)
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(new InetSocketAddress(host, port));
// Selector.select(timeout) → 可中断
// 方案3:改用支持超时的HTTP客户端
// JDK 11+ HttpClient 自动支持超时和中断
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
教训:
- 所有I/O操作必须设置合理的超时时间,不要依赖平台默认值
- 使用NIO/异步I/O而非传统阻塞I/O,后者对中断信号的响应不可靠
- 优雅关闭应预留合理的超时时间(通常30-60秒),超时后接受force kill
4.5 思考题
Q1: 假设订单处理线程池配置为core=4, max=8, queue=ArrayBlockingQueue(100)。业务高峰期,每个订单处理耗时约200ms,瞬时QPS为60。请计算线程池是否会触发拒绝策略?如果会,大约在提交多少个任务后触发?如何调整配置避免拒绝?(请写出详细计算过程)
参考答案
处理能力计算:
- 4个核心线程 + 100个队列任务 = 同时可承载104个任务
- 每个任务耗时200ms,单线程QPS = 1000ms/200ms = 5 QPS
- 4个核心线程的总处理QPS = 4 × 5 = 20 QPS
- 瞬时QPS(60)远大于处理QPS(20),差值40 QPS
- 队列100个任务会被填满的时间:100 / 40 = 2.5秒
- 第105个任务提交时,4个核心线程都在忙,队列满100个 → 创建第5个线程
- 同理第106→8个线程依次创建,第109个任务触发拒绝
- 总触发时间约:2.5s + (4个线程创建时间) ≈ 2.6秒后开始拒绝
调整方案:
- 增加corePoolSize到12(200ms × 12 = 2400ms capacity, 12线程 × 5QPS = 60QPS刚好匹配),队列作为缓冲保留50
- 或使用CallerRunsPolicy实现背压(但需注意提交线程不能是Tomcat Worker线程)
- 或前置引入Sentinel/Resilience4j限流,在提交线程池之前就拒绝超量请求
Q2: 在JDK 21虚拟线程中使用ThreadLocal与平台线程中使用ThreadLocal相比,泄漏影响有什么不同?JDK 21的ScopedValue如何从设计上避免ThreadLocal的泄漏问题?请结合两者源码设计进行对比分析。
参考答案
泄漏影响差异:
- 平台线程:线程池固定200个,ThreadLocal最多泄漏200份Value对象,影响可控
- 虚拟线程:每个请求一个虚拟线程,高峰期可能同时存在数万甚至数十万虚拟线程。如果每个虚拟线程都set ThreadLocal而不remove,泄漏对象数量正比于并发虚拟线程数,内存影响被严重放大
ScopedValue的防泄漏设计:
- ScopedValue本身不可变(类似final变量),值的生命周期由作用域严格界定
ScopedValue.where(key, value).run(runnable)的运行模式确保run()结束后值自动失效- 实现方式:通过栈帧上的绑定而非ThreadLocalMap的哈希表,天然无泄漏
- 虚拟线程对其有原生支持:挂起/恢复虚拟线程时,ScopedValue绑定随栈帧自动保存/恢复
- 对比ThreadLocal的ThreadLocalMap(需要手动remove),ScopedValue从API层面消除了泄漏可能性
源码层面差异:
-
ThreadLocal: 值存储在
Thread.threadLocals(ThreadLocalMap),生命周期与Thread一致 -
ScopedValue: 值存储在当前调用栈的
Continuation帧中,生命周期与作用域绑定
下一章预告:第20章将深入G1 GC原理、日志与调优实战——现代Java服务默认GC的生产级调优指南。我们将从G1的Region布局、SATB并发标记、Mixed GC机制讲起,结合GC日志分析和
-XX参数调优,帮助你建立起系统的GC问题排查框架。
延伸阅读与资源
Java 工程师进阶:从 JVM 生产排障到OpenJDK原理
Nacos 3.x 注册配置中心实战修炼:从入门到源码扩展
Elasticsearch从入门到进阶的实战之旅
MySQL Server 9从入门到进阶的实战之旅
RabbitMQ从入门到进阶的实战之旅:从单机到大促高可用架构
Celery 入门到进阶之路:从异步任务到自研调度平台
LangGraph 生产级实战进阶:从零到生产级Agent工作流开发
Dify 从入门到源码:LLM 应用平台实战修炼
从零到生产级:FastAPI 异步高并发、源码与 SRE 实战
实战SQLAlchemy 2.0: 从 CRUD 到生产级架构
从零打造企业级 AI 助手:LangChain RAG、Agent 与生产实战
后端工程师 AI 转型课:Ollama 私有化大模型从入门到生产
MongoDB 实战进阶与内核修炼
NumPy 从入门到生产落地:全链路实战指南(科学计算/向量化/性能调优)
Milvus向量数据库实战修炼:从 0 到 1 精通向量检索与生产落地
Redis 8 实战精讲:从 CRUD 到源码,构建高可用缓存系统
Python 3实战精进:从脚本到高并发订单引擎
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经

微信公众号: 架构师日常笔记 欢迎关注!
浙公网安备 33010602011771号