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 在提交任务时,逻辑顺序严格如下:
当前线程数 < corePoolSize,创建核心线程
否则,尝试将任务放入队列
队列满,且线程数 < maximumPoolSize,创建非核心线程
仍然失败,触发拒绝策略
理解这个顺序,是设计参数的前提。
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 一个可落地的设计流程
明确任务类型
估算最大并发
选择队列策略
设置拒绝策略
压测验证