从零起步学习并发编程 || 第八章:线程池实战(避坑指南与最佳实践)

一、为什么要使用线程池?

不使用线程池直接创建线程会带来严重问题:

  1. 资源开销大:频繁创建/销毁线程消耗大量CPU和内存资源,线程创建本身是重量级操作

  2. 系统不稳定:无限制创建线程可能导致:

    • 内存溢出(OOM):每个线程需分配栈空间(默认1MB)
    • 上下文切换频繁:线程过多导致CPU在切换上消耗过多资源
    • 系统崩溃:耗尽操作系统线程资源
  3. 线程池的核心价值

    • 复用线程:避免重复创建销毁的开销
    • 控制并发:限制最大线程数,保护系统资源
    • 任务排队:通过队列平滑处理突发流量
    • 统一管理:监控、统计、优雅关闭等能力

二、为什么不推荐使用Executors内置线程池?

阿里巴巴Java开发手册明确强制规定 :线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor方式。原因如下:

Executors方法 风险点 具体问题
newFixedThreadPool / newSingleThreadExecutor 队列无界 内部使用LinkedBlockingQueue(默认容量Integer.MAX_VALUE),任务堆积会导致OOM博客园
newCachedThreadPool 线程数无界 最大线程数为Integer.MAX_VALUE,高并发下可能创建海量线程导致系统崩溃知乎
newScheduledThreadPool 队列无界 同样使用无界队列,存在内存溢出风险

核心问题Executors封装过度,隐藏了关键参数(如队列容量、拒绝策略),开发者无法感知资源风险,容易在生产环境引发事故

正确做法 :显式使用ThreadPoolExecutor构造函数,明确指定所有参数:

复制代码
java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5,                          // corePoolSize
    10,                         // maximumPoolSize
    60L,                        // keepAliveTime
    TimeUnit.SECONDS,           // unit
    new LinkedBlockingQueue<>(100), // 有界队列!
    new CustomThreadFactory(),  // 可自定义线程名
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);

三、线程池核心参数(ThreadPoolExecutor 7个参数)

复制代码
arduino 复制代码
public ThreadPoolExecutor(
    int corePoolSize,           // 核心线程数
    int maximumPoolSize,        // 最大线程数
    long keepAliveTime,         // 空闲线程存活时间
    TimeUnit unit,              // 时间单位
    BlockingQueue<Runnable> workQueue, // 任务队列
    ThreadFactory threadFactory,// 线程工厂
    RejectedExecutionHandler handler   // 拒绝策略
)
参数 说明 最佳实践
corePoolSize 核心线程数,即使空闲也会保留 根据CPU核心数和任务类型设置: • CPU密集型:N+1(N为CPU核心数) • IO密集型:2N或更高阿里云官方网站
maximumPoolSize 最大线程数,队列满后可扩容至此值 需结合队列容量设置,避免无界增长
keepAliveTime 非核心线程空闲存活时间 通常设为30-60秒,平衡资源回收与创建开销
workQueue 任务队列 必须使用有界队列 (如ArrayBlockingQueue),避免OOM知乎
threadFactory 线程工厂 自定义线程名(如"order-task-pool-%d"),便于排查问题
handler 拒绝策略 根据业务场景选择(见下文)

执行流程

  1. 当前运行线程数 < corePoolSize → 创建新核心线程执行
  2. 达到corePoolSize → 任务入队workQueue
  3. 队列满且线程数 < maximumPoolSize → 创建非核心线程
  4. 队列满且达到maximumPoolSize → 触发拒绝策略

四、线程池拒绝策略(4种内置 + 自定义)

线程数达到maximumPoolSize + 队列已满时触发拒绝策略:

策略 行为 适用场景
AbortPolicy(默认) 抛出RejectedExecutionException异常 通用场景,快速失败便于发现问题腾讯
CallerRunsPolicy 由提交任务的线程(调用者)直接执行 降低提交速度,适用于允许降级的场景
DiscardPolicy 静默丢弃任务,不抛异常 允许任务丢失的场景(如日志上报)
DiscardOldestPolicy 丢弃队列中最老的任务,尝试重新提交新任务 保留最新任务的场景(如实时数据处理)

自定义拒绝策略示例

复制代码
typescript 复制代码
public class CustomRejectPolicy implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 1. 记录日志告警
        log.warn("Task rejected, queue size: {}", executor.getQueue().size());
        
        // 2. 可选:持久化到DB/消息队列,后续重试
        // taskPersistence.save(r);
        
        // 3. 可选:降级处理
        // fallbackService.handle(r);
        
        throw new RejectedExecutionException("Task rejected due to overload");
    }
}

五、生产环境最佳实践

  1. 必须使用有界队列new ArrayBlockingQueue<>(100),避免OOM

  2. 监控关键指标

    复制代码
    scss 复制代码
    executor.getActiveCount();      // 活跃线程数
    executor.getQueue().size();     // 队列堆积量
    executor.getCompletedTaskCount();// 完成任务数
  3. 优雅关闭

    复制代码
    scss 复制代码
    executor.shutdown(); // 停止接收新任务,等待已有任务完成
    // 或
    executor.shutdownNow(); // 立即中断所有任务
  4. 命名规范 :通过ThreadFactory设置有意义的线程名,便于排查问题

  5. 参数调优 :根据压测结果动态调整corePoolSize/queueCapacity

    知乎

💡 总结 :线程池是双刃剑------用得好提升系统吞吐量,用不好直接导致生产事故。务必显式创建ThreadPoolExecutor,明确所有参数,尤其是队列必须有界,并根据业务选择合适的拒绝策略。

一、线程池处理任务的完整流程

ThreadPoolExecutor 的任务处理遵循以下四步决策链

关键细节

  • 核心线程默认不会超时回收 (除非调用 allowCoreThreadTimeOut(true)

  • 非核心线程在空闲超过 keepAliveTime 后会被回收

  • 队列类型影响行为:

    • SynchronousQueue:不存储任务,必须立即有空闲线程,否则创建新线程(适合 CachedThreadPool
    • LinkedBlockingQueue / ArrayBlockingQueue:先入队,队列满才扩容线程

二、线程异常后:销毁还是复用?

线程会被复用,但需正确处理异常,否则可能"静默死亡"导致线程池失效。

问题场景(错误写法):

复制代码
php 复制代码
executor.execute(() -> {
    throw new RuntimeException("任务异常"); // 未捕获 → 线程终止,任务丢失
});

此线程会因未捕获异常而退出,线程池会创建新线程替代,但任务已丢失且无日志,极难排查。

正确做法(3种方案):

方案1:任务内部 try-catch(推荐)

复制代码
scss 复制代码
executor.execute(() -> {
    try {
        // 业务逻辑
        process();
    } catch (Exception e) {
        log.error("任务执行异常", e);
        // 可选:告警、降级、重试
    }
});

方案2:自定义 ThreadFactory 捕获未处理异常

复制代码
ini 复制代码
ThreadFactory namedFactory = r -> {
    Thread t = new Thread(r, "biz-task-" + seq.incrementAndGet());
    t.setUncaughtExceptionHandler((thread, ex) -> {
        log.error("线程[{}]未捕获异常", thread.getName(), ex);
        monitor.alert("ThreadPoolUncaughtException", ex);
    });
    return t;
};

方案3:继承 ThreadPoolExecutor 重写 afterExecute

复制代码
scala 复制代码
public class SafeThreadPool extends ThreadPoolExecutor {
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        if (t == null && r instanceof Future<?>) {
            try {
                ((Future<?>) r).get(); // 获取异步任务异常
            } catch (CancellationException ce) {
                t = ce;
            } catch (ExecutionException ee) {
                t = ee.getCause();
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
            }
        }
        if (t != null) {
            log.error("任务执行异常", t);
        }
    }
}

结论:线程异常不会自动销毁整个线程池,但未捕获异常会导致该线程退出,线程池会创建新线程补充。务必通过上述方式捕获异常,避免任务静默失败。


三、如何给线程命名?

使用 ThreadFactory 自定义线程名,便于日志追踪和问题定位:

复制代码
java 复制代码
public class NamedThreadFactory implements ThreadFactory {
    private final AtomicInteger seq = new AtomicInteger(1);
    private final String namePrefix;
    private final boolean daemon;

    public NamedThreadFactory(String namePrefix, boolean daemon) {
        this.namePrefix = namePrefix;
        this.daemon = daemon;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, namePrefix + "-" + seq.getAndIncrement());
        t.setDaemon(daemon);
        t.setUncaughtExceptionHandler((thread, ex) ->
            log.error("线程[{}]异常退出", thread.getName(), ex)
        );
        return t;
    }
}

// 使用
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5, 10, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),
    new NamedThreadFactory("order-service", false),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

命名规范建议{业务模块}-{功能}-{序号},如 pay-async-01user-query-03


四、如何动态修改线程池参数?

ThreadPoolExecutor 提供线程安全的 setter 方法,支持运行时调参:

复制代码
scss 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(...);

// 动态调整核心线程数
executor.setCorePoolSize(8);

// 动态调整最大线程数
executor.setMaximumPoolSize(20);

// 动态调整空闲超时时间
executor.setKeepAliveTime(30L, TimeUnit.SECONDS);

// 允许核心线程超时回收(默认false)
executor.allowCoreThreadTimeOut(true);

⚠️ 注意事项:

参数 调整方向 影响
corePoolSize 增大 立即生效 可能立即创建新线程
corePoolSize 减小 延迟生效 现有核心线程不会立即销毁,需等空闲超时
maximumPoolSize 立即生效 影响后续扩容上限
keepAliveTime 立即生效 影响后续空闲线程回收

相关推荐
Java水解1 小时前
【Spring Cloud】优雅实现远程调用-OpenFeign
后端·spring cloud
wefg12 小时前
【Linux】信号的产生、保存、处理
linux·运维·服务器
peng_YuJun2 小时前
openEuler 虚拟机从零到一:完整部署指南
linux·运维·服务器·vmware·openeuler
迪巴拉15252 小时前
基于Springboot+Vue的制造业采购管理系统
vue.js·spring boot·后端
Remember_9932 小时前
SpringCloud:Nacos注册中心
java·开发语言·后端·算法·spring·spring cloud·list
红豆子不相思2 小时前
Keepalived
运维·服务器·网络
JavaLearnerZGQ2 小时前
SpringAI中的ChatModel是啥
后端
qq_12498707532 小时前
基于springboot+vue的热门文创内容推荐平台(源码+论文+部署+安装)
vue.js·spring boot·后端·spring·毕业设计·计算机毕设
Trouvaille ~2 小时前
【动态规划篇】专题(一):斐波那契模型——从数学递推到算法思维
c++·算法·leetcode·青少年编程·面试·动态规划·入门