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

0 阅读7分钟

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

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

  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构造函数,明确指定所有参数:

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

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

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丢弃队列中最老的任务,尝试重新提交新任务保留最新任务的场景(如实时数据处理)

自定义拒绝策略示例

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. 监控关键指标

    executor.getActiveCount();      // 活跃线程数
    executor.getQueue().size();     // 队列堆积量
    executor.getCompletedTaskCount();// 完成任务数
    

  3. 优雅关闭

    executor.shutdown(); // 停止接收新任务,等待已有任务完成
    // 或
    executor.shutdownNow(); // 立即中断所有任务
    

  4. 命名规范:通过ThreadFactory设置有意义的线程名,便于排查问题

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

    知乎

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

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

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

关键细节

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

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

  • 队列类型影响行为:

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

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

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

问题场景(错误写法):

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

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

正确做法(3种方案):

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

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

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

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

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 自定义线程名,便于日志追踪和问题定位:

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 方法,支持运行时调参:

ThreadPoolExecutor executor = new ThreadPoolExecutor(...);

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

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

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

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

⚠️ 注意事项:

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