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个典型)

  1. Web服务器请求处理:Tomcat/Jetty的请求处理线程池,控制并发请求数避免系统过载。典型配置:core=CPU核数,max=CPU核数*2,队列=1000。

  2. 异步消息消费:RocketMQ/Kafka消费者线程池,并行消费提高吞吐。建议core=max=分区数,队列使用ArrayBlockingQueue拒绝时告警。

  3. 批量数据导出:控制并发导出任务数,避免数据库连接池耗尽。典型配置:core=max=数据库连接池大小/2,使用CallerRunsPolicy。

  4. 定时任务调度:ScheduledThreadPoolExecutor用于延迟执行和周期性任务,替代Timer(单线程,异常会终止调度)。

  5. 网关限流熔断:API网关使用线程池隔离不同服务的调用,实现舱壁模式(Bulkhead),防止某个慢服务拖垮整个网关。

线程池不适用场景(2个)

  1. 纯CPU密集型计算:线程切换带来的上下文切换开销反而降低吞吐。应使用ForkJoinPool或直接使用核心数数量的线程池,队列使用SynchronousQueue。

  2. 极低延迟场景(微秒级):线程池的锁竞争和上下文切换会破坏延迟确定性。应使用Disruptor等无锁框架或协程方案。

ThreadLocal适用场景(5个典型)

  1. 分布式链路追踪:存储traceId、spanId,贯穿整个请求生命周期,无需在每个方法签名中传递。

  2. Spring事务管理:TransactionSynchronizationManager使用ThreadLocal存储当前线程的事务资源和同步状态。

  3. 数据库读写分离:使用ThreadLocal存储数据源路由key(如"master"/"slave"),在ORM层透明切换。

  4. 用户会话上下文:存储当前登录用户信息(userId、tenantId、权限列表),避免在业务层重复查询。

  5. 日期格式化:SimpleDateFormat非线程安全,使用ThreadLocal为每个线程分配独立实例(JDK 8+建议直接使用DateTimeFormatter)。

ThreadLocal不适用场景(2个)

  1. 高并发虚拟线程场景:每个虚拟线程一个ThreadLocal副本,大量短生命周期虚拟线程导致对象堆积。建议使用ScopedValue。

  2. 跨线程共享状态: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"。

根因分析:

  1. 代码中使用了Executors.newCachedThreadPool()处理第三方支付回调
  2. 促销期间回调量暴增,SynchronousQueue无法缓冲,线程数从几十暴涨到8000+
  3. 每个线程默认栈大小1MB(64位JVM),仅线程栈就消耗8GB+内存
  4. Pod配置的4G内存被吃光,Linux OOM Killer介入杀死JVM进程
  5. 线程创建开销(分配栈内存、初始化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,必须重启恢复。

根因分析:

  1. 应用使用自定义的RequestContextFilter,在每个请求开始时设置ThreadLocal存储userInfo
  2. Filter中只在正常流程中调用了remove(),但异常流程(如权限校验失败抛出异常)遗漏了remove()
  3. 线程池使用的是Tomcat默认线程池(maxThreads=200),线程长期存活
  4. 每次热部署创建新的类加载器,但线程池中的200个线程仍持有旧类加载器加载的RequestContextFilter类的ThreadLocal引用
  5. JDK 17移除了ThreadLocal的finalize()方法,失去了最后的"钩子清理"机会
  6. 旧类加载器和它加载的所有类无法被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,导致正在处理中的订单数据不一致。

根因分析:

  1. 订单处理线程池(10个线程)中有一个任务在执行Socket.connect()连接第三方支付网关
  2. 第三方支付网关因网络问题无响应,Socket.connect()默认超时时间为平台相关(Linux约20分钟)
  3. shutdownNow()调用Thread.interrupt(),但Socket.connect()属于不可中断的I/O阻塞(NIO的SocketChannel.connect()才是可中断的)
  4. 线程在阻塞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秒后开始拒绝

调整方案:

  1. 增加corePoolSize到12(200ms × 12 = 2400ms capacity, 12线程 × 5QPS = 60QPS刚好匹配),队列作为缓冲保留50
  2. 或使用CallerRunsPolicy实现背压(但需注意提交线程不能是Tomcat Worker线程)
  3. 或前置引入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的网络实战圣经

posted on 2026-10-01 09:23  一天不进步,就是退步  阅读(7)  评论(0)    收藏  举报