enyang
enyang
Published on 2026-01-18 / 1 Visits
0
0

Java 多线程<7>——线程池原理与参数设计

1 为什么不能随意创建线程

在并发程序的早期阶段,最常见的写法是直接创建线程:

new Thread(() -> {
    // 执行任务
}).start();

这种方式在功能上没有问题,但在工程层面存在严重隐患。

第一,线程创建和销毁成本高。线程不仅占用内存,还需要操作系统调度,频繁创建会严重影响性能。

第二,线程数量不可控。一旦请求激增,系统可能同时创建大量线程,导致上下文切换风暴,甚至触发 OOM。

第三,缺乏统一管理。线程无法复用,无法统计,无法优雅关闭。

线程池的本质目标,是用有限的线程,稳定地处理无限的任务。

2 Executor 框架的整体设计

2.1 Executor 的抽象层次

Java 并没有一开始就暴露 ThreadPoolExecutor,而是通过分层抽象逐步构建线程池体系。

  • Executor 只定义了任务提交行为

  • ExecutorService 扩展了生命周期管理能力

  • ThreadPoolExecutor 是最核心的实现

这种设计将“提交任务”和“如何执行任务”彻底解耦。

2.2 线程池解决的核心问题

线程池统一解决了以下问题:

  • 线程复用

  • 并发数量控制

  • 任务缓存

  • 任务拒绝

  • 线程生命周期管理

3 ThreadPoolExecutor 的核心结构

3.1 七大核心参数

ThreadPoolExecutor 的构造函数定义了线程池的全部行为:

public ThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler
)

每一个参数都不是可选项,而是系统设计决策。

3.2 corePoolSize 的真实含义

corePoolSize 并不是“最小线程数”,而是长期存活线程数。

  • 即使线程空闲,也不会被回收

  • 任务到来时,优先创建核心线程

  • 核心线程满了,才会进入队列

corePoolSize 决定了系统的基础并发能力。

3.3 maximumPoolSize 的控制边界

maximumPoolSize 是线程池能容忍的最大并发线程数量。

只有在以下条件同时满足时,才会创建非核心线程:

  • 核心线程已满

  • 队列已满

  • 当前线程数小于 maximumPoolSize

这意味着,maximumPoolSize 是系统的最后兜底保护线。

3.4 keepAliveTime 的回收策略

keepAliveTime 用于控制非核心线程的存活时间。

  • 非核心线程空闲超过该时间会被回收

  • 核心线程默认不会被回收

  • 可通过 allowCoreThreadTimeOut 修改行为

该参数用于在“峰值”和“常态”之间做资源平衡。

4 任务队列的设计取舍

4.1 队列决定线程池性格

线程池的行为,很大程度上由 workQueue 决定,而不是线程数量。

常见误区是只调线程数,却忽略队列类型。

4.2 LinkedBlockingQueue 的风险

new LinkedBlockingQueue<>();

这是 Executors 默认使用的队列,特点是容量极大(接近无界)。

优点是:

  • 线程数稳定

  • 任务不易被拒绝

缺点是:

  • 任务无限堆积

  • 内存压力不可控

  • 延迟不可预测

在高并发系统中,这是最危险的选择。

4.3 ArrayBlockingQueue 的可控性

new ArrayBlockingQueue<>(1000);

有界数组队列的优势在于:

  • 容量明确

  • 内存可控

  • 反压机制清晰

它强迫系统在高负载下做出选择,而不是悄悄堆积风险。

4.4 SynchronousQueue 的极端策略

SynchronousQueue 没有容量,每个任务必须直接交给线程执行。

  • 不缓存任务

  • 强依赖 maximumPoolSize

  • 适合短任务、高吞吐场景

这是吞吐优先、延迟敏感系统的选择,但配置错误风险极高。

5 线程池的任务提交流程

5.1 execute 方法的完整路径

ThreadPoolExecutor 在提交任务时,逻辑顺序严格如下:

  1. 当前线程数 < corePoolSize,创建核心线程

  2. 否则,尝试将任务放入队列

  3. 队列满,且线程数 < maximumPoolSize,创建非核心线程

  4. 仍然失败,触发拒绝策略

理解这个顺序,是设计参数的前提。

5.2 一个典型的参数组合分析

corePoolSize = 8
maximumPoolSize = 16
queueCapacity = 1000

这种配置意味着:

  • 并发压力先由队列吸收

  • 线程扩容较慢

  • 延迟可能增加

  • 系统稳定性较高

是否合理,取决于业务是否允许排队。

6 拒绝策略的工程意义

6.1 四种内置拒绝策略

  • AbortPolicy 直接抛异常

  • CallerRunsPolicy 由提交线程执行

  • DiscardPolicy 直接丢弃

  • DiscardOldestPolicy 丢弃队列中最老任务

拒绝不是错误,而是系统的自我保护机制。

6.2 CallerRunsPolicy 的反压效果

CallerRunsPolicy 会迫使上游线程执行任务,从而:

  • 降低任务提交速度

  • 将压力反向传播

  • 防止线程池被击穿

这是很多高稳定性系统的首选策略。

6.3 自定义拒绝策略

class LogRejectHandler implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 记录日志、告警、降级处理
    }
}

拒绝策略是系统稳定性的最后防线,必须显式设计。

7 线程池的线程工厂

7.1 为什么要自定义线程工厂

默认线程名称不可读,异常难以定位。

自定义 ThreadFactory 可以:

  • 统一命名

  • 设置优先级

  • 捕获未处理异常

7.2 示例线程工厂

class NamedThreadFactory implements ThreadFactory {
    private final AtomicInteger index = new AtomicInteger(1);

    public Thread newThread(Runnable r) {
        return new Thread(r, "biz-thread-" + index.getAndIncrement());
    }
}

线程命名是排查线上问题的基础能力。

8 线程池的关闭语义

8.1 shutdown 与 shutdownNow 的区别

shutdown 会:

  • 停止接收新任务

  • 继续执行已提交任务

shutdownNow 会:

  • 尝试中断正在执行的线程

  • 返回未执行任务列表

错误使用 shutdownNow,可能直接导致业务中断。

8.2 优雅关闭的基本模式

executor.shutdown();
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
    executor.shutdownNow();
}

这是生产系统中推荐的关闭方式。

9 Executors 工厂方法的陷阱

9.1 为什么不推荐 Executors

Executors 提供的便捷方法隐藏了关键参数:

  • newFixedThreadPool 使用无界队列

  • newCachedThreadPool 使用无限线程

  • newSingleThreadExecutor 可能任务堆积

这些方法适合 demo,不适合生产。

10 线程池参数设计方法论

10.1 基于任务类型分类

  • CPU 密集型:线程数接近 CPU 核数

  • IO 密集型:线程数可大于 CPU 核数

  • 混合型:拆分线程池

10.2 一个可落地的设计流程

  1. 明确任务类型

  2. 估算最大并发

  3. 选择队列策略

  4. 设置拒绝策略

  5. 压测验证


Comment