深入理解 Java 线程池:从 7 个核心参数到线上事故排查

举报
yd_231768394 发表于 2026/10/21 21:27:53 2026/10/21
【摘要】 线程池几乎是大厂面试的必考点,也是线上问题的高发地。这篇文章不讲 API 怎么调,只讲原理、坑和排查手段。 一、为什么需要线程池每来一个任务就 new Thread() 有三个致命问题:创建销毁开销大:线程栈默认 1MB,频繁创建会加剧 GC 与上下文切换;无法控制并发量:流量突增时线程数可能被打到几千个,直接拖垮机器;缺少统一管理:没有队列、没有拒绝策略、没有监控指标。线程池本质是一个生产...

线程池几乎是大厂面试的必考点,也是线上问题的高发地。这篇文章不讲 API 怎么调,只讲原理、坑和排查手段。

一、为什么需要线程池

每来一个任务就 new Thread() 有三个致命问题:

  1. 创建销毁开销大:线程栈默认 1MB,频繁创建会加剧 GC 与上下文切换;
  2. 无法控制并发量:流量突增时线程数可能被打到几千个,直接拖垮机器;
  3. 缺少统一管理:没有队列、没有拒绝策略、没有监控指标。

线程池本质是一个生产者—消费者模型:任务进队列,固定数量的 Worker 循环取任务执行。

public void execute(Runnable command) {
    if (command == null) throw new NullPointerException();
    int c = ctl.get();
    // 1. 工作线程数 < corePoolSize,直接新建核心线程
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true)) return;
        c = ctl.get();
    }
    // 2. 核心线程已满,入队
    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);
    }
    // 3. 入队失败(队列满),尝试扩容到 maximumPoolSize
    else if (!addWorker(command, false))
        reject(command); // 4. 仍然失败,走拒绝策略
}

这段源码是理解线程池的唯一关键,记住一条结论:

只有当队列满了之后,线程池才会把线程数从 corePoolSize 扩到 maximumPoolSize。

二、7 个参数的真实含义

参数 作用 易错点
corePoolSize 核心线程数,默认常驻 设置 allowCoreThreadTimeOut(true) 才可回收
maximumPoolSize 最大线程数 队列没满时它是摆设
keepAliveTime + unit 非核心线程空闲存活时间 只对超出 core 的线程生效
workQueue 任务排队队列 用无界队列等于 maximumPoolSize 失效
threadFactory 线程工厂 必须自定义命名,否则 dump 全是 pool-1-thread-1
handler 拒绝策略 默认 AbortPolicy 直接抛异常

队列选择决定行为

  • LinkedBlockingQueue(默认容量 Integer.MAX_VALUE):等于无界队列,任务会无限堆积直至 OOM,maximumPoolSize 永远用不上;
  • ArrayBlockingQueue:有界队列,可实现"先扩线程、再拒绝"的预期行为;
  • SynchronousQueue:不存储元素,来一个任务必须立刻有线程接,配合大 max 就是 CachedThreadPool 的模型;
  • DelayedWorkQueue/PriorityBlockingQueue:前者给 ScheduledThreadPoolExecutor 用,后者任务需实现 Comparable。

三、四种拒绝策略怎么选

new ThreadPoolExecutor.AbortPolicy();        // 抛 RejectedExecutionException(默认)
new ThreadPoolExecutor.CallerRunsPolicy();  // 由提交任务的线程自己执行,天然反压
new ThreadPoolExecutor.DiscardPolicy();     // 静默丢弃,几乎不要用
new ThreadPoolExecutor.DiscardOldestPolicy();// 丢弃队首最老任务,再重试

生产建议:默认用 AbortPolicy,并且必须捕获异常或使用自定义策略打日志。 DiscardPolicy 是最危险的选择——任务凭空消失,排查时毫无线索。

自定义拒绝策略模板:

public class LoggingRejectPolicy implements RejectedExecutionHandler {
    private static final Logger log = LoggerFactory.getLogger(LoggingRejectPolicy.class);

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        log.error("task rejected, pool={}, active={}, queue={}, completed={}",
                e.getPoolSize(), e.getActiveCount(),
                e.getQueue().size(), e.getCompletedTaskCount());
        throw new RejectedExecutionException("thread pool is saturated");
    }
}

四、五个高频线上事故

1. 无界队列导致内存溢出

// 反面教材
Executors.newFixedThreadPool(10);
// 等价于 new ThreadPoolExecutor(10, 10, 0L, MILLISECONDS,
//            new LinkedBlockingQueue<>());  // 容量 = Integer.MAX_VALUE

任务生产速度 > 消费速度时,队列会一直增长,最终 OutOfMemoryError: Java heap space。必须换成有界队列,并在队列满时让上游感知到压力(拒绝 + 重试/降级)。

2. 提交的任务异常被静默吞掉

Future<?> f = pool.submit(() -> { throw new RuntimeException("boom"); });
// 不调用 f.get(),异常永远不会出现在日志里

submit() 会把异常封装进 Future,只有 get() 才抛出;而 execute() 会交给线程的 UncaughtExceptionHandler。

解决方案:execute() 提交,或重写 afterExecute 统一处理。

@Override
protected void afterExecute(Runnable r, Throwable t) {
    super.afterExecute(r, t);
    if (t == null && r instanceof Future<?> future && future.isDone()) {
        try {
            future.get();
        } catch (ExecutionException e) {
            log.error("task failed", e.getCause());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

3. 父子任务同池 → 死锁

父任务占用全部线程并阻塞等待子任务,子任务在队列里永远排不到线程。不同语义的任务必须用不同线程池(IO 池、计算池、定时任务池物理隔离)。

4. ThreadLocal 在线程复用下残留数据

try {
    USER_CONTEXT.set(user);
    doWork();
} finally {
    USER_CONTEXT.remove();  // 必须写在 finally 里
}

线程池中的线程不会销毁,ThreadLocalMap 的 key 是弱引用而被回收,value 是强引用却滞留,形成内存泄漏。更严重的是数据串号——下一个请求可能读到上一个用户的信息。

5. shutdown() 之后任务还在跑

  • shutdown():不接受新任务,执行完队列中的任务;
  • shutdownNow():中断所有线程,返回未执行的任务列表;
  • awaitTermination(timeout, unit):等待收敛,关闭时必须成对使用,否则主线程先退出、任务被静默丢弃。
pool.shutdown();
if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
    List<Runnable> dropped = pool.shutdownNow();
    log.warn("force shutdown, {} tasks dropped", dropped.size());
}

五、线程数到底设多少

  • CPU 密集型:N + 1(N 为核数),多出来的 1 用于偶发缺页中断;
  • IO 密集型:N × (1 + 等待时间 / 计算时间),工程上常取 2N 起步,再压测微调;
  • 不要拍脑袋:线程数取决于下游 RT。数据库连接池只有 20 个连接,线程开到 200 只会让请求排队在连接池上,并把超时错误放大。

六、一个可直接用的生产级工厂

public final class ThreadPools {
    public static ThreadPoolExecutor build(String name, int core, int max,
                                           int queueCapacity, long keepAliveSec) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                core, max, keepAliveSec, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(queueCapacity),
                new NamedThreadFactory(name),
                new LoggingRejectPolicy());
        pool.allowCoreThreadTimeOut(true); // 低峰期回收,节省资源
        return pool;
    }

    static class NamedThreadFactory implements ThreadFactory {
        private final String prefix;
        private final AtomicInteger seq = new AtomicInteger(1);

        NamedThreadFactory(String prefix) { this.prefix = prefix; }

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r, prefix + "-" + seq.getAndIncrement());
            t.setDaemon(false);
            t.setUncaughtExceptionHandler((th, e) ->
                    log.error("uncaught exception in {}", th.getName(), e));
            return t;
        }
    }
}

关键点只有三条:有界队列 + 有名字的线程 + 拒绝时打日志。

七、监控看什么

指标 含义 报警阈值参考
getActiveCount() 正在执行的线程数 持续 ≈ poolSize
getQueue().size() 排队任务数 持续 > 容量的 80%
getCompletedTaskCount() 已完成任务数 增长停滞即异常
getLargestPoolSize() 历史峰值线程数 判断 max 是否合理

最小可用方案:ScheduledExecutorService 每 10s 采集一次,打点到 Micrometer/Prometheus。

Gauge.builder("executor.queue.size", pool, p -> p.getQueue().size())
     .tag("pool", "order-async").register(meterRegistry);

结语

线程池的所有坑,归根到底都来自同一个误解:把线程池当成"能自动扩容的万能池"。它的容量由队列与 max 共同决定,它的失败必须被显式处理,它的线程是有状态的(ThreadLocal、上下文、局部缓存都需清理)。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。