线程池

线程池核心参数与执行流程

1. 我对线程池的理解

我把线程池理解成一个 任务调度容器
它内部提前准备或按需创建一些线程,用来反复执行提交进来的任务。
这样做的核心价值有三点:

  1. 复用线程,减少频繁创建和销毁线程的开销
  2. 控制并发数量,避免线程无限增长
  3. 通过阻塞队列缓存任务,削峰填谷

Java 里最核心的线程池实现类是 ThreadPoolExecutor


2. 核心构造参数

ThreadPoolExecutor 最常见的构造方法如下:

1
2
3
4
5
6
7
8
9
public ThreadPoolExecutor(  
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)

我可以把这 7 个参数理解成:

  • corePoolSize:线程池平时保留多少个核心线程
  • maximumPoolSize:线程池最多允许多少个线程
  • keepAliveTime:多出来的空闲线程最多活多久
  • unit:上面这个时间的单位
  • workQueue:任务太多时先排队到哪里
  • threadFactory:线程要怎么被创建
  • handler:线程和队列都满了之后怎么办

3. 每个参数的具体含义

3.1 corePoolSize

corePoolSize 表示 核心线程数

我把核心线程理解成线程池里的常驻员工。
任务刚提交进来时,线程池会优先创建核心线程来处理任务。
即使这些核心线程后面空闲了,默认也会保留,不会立刻销毁。

例如:

corePoolSize = 5

我的理解是:

  • 线程池优先维持 5 个核心线程
  • 前 5 个任务到来时,大概率会直接创建线程执行
  • 这 5 个线程之后会尽量长期存在,反复处理后续任务

补充一点:

如果调用了下面这个方法:

executor.allowCoreThreadTimeOut(true);

那核心线程也允许因空闲超时而被回收。
也就是说,核心线程默认常驻,但也可以改成不常驻。


3.2 maximumPoolSize

maximumPoolSize 表示 线程池允许存在的最大线程数

这里说的是总线程数,不只是非核心线程数。

例如:

corePoolSize = 5
maximumPoolSize = 10

我的理解是:

  • 前 5 个是核心线程
  • 当核心线程都忙,任务队列也满了,线程池才会继续创建新线程
  • 新增出来的这部分线程属于非核心线程
  • 总线程数最多只能到 10

所以我可以记成:

corePoolSize 决定平时保底有多少线程
maximumPoolSize 决定高峰期最多能扩到多少线程


3.3 keepAliveTime

keepAliveTime 表示 空闲线程的存活时间

默认情况下,它主要作用于 非核心线程
也就是说,一个非核心线程如果空闲时间超过这个值,就可能被销毁。

例如:

keepAliveTime = 60
unit = TimeUnit.SECONDS

我的理解是:

  • 一个非核心线程如果连续 60 秒没有拿到任务
  • 线程池就可以把它回收掉
  • 这样高峰期扩出来的线程,不会一直占着资源

所以它的作用,本质上是控制线程池的“弹性收缩”。


3.4 unit

unit 就是 keepAliveTime 的时间单位。

常见写法有:

TimeUnit.SECONDS
TimeUnit.MILLISECONDS
TimeUnit.MINUTES

这个参数本身没什么复杂逻辑,它只是告诉线程池应该按什么单位解释 keepAliveTime


3.5 workQueue

workQueue 表示 任务阻塞队列

当核心线程都在忙时,新任务通常不会立刻创建新线程,而是先尝试进入这个队列等待。

我把它理解成“候车区”或者“排队区”。

常见队列类型

1)ArrayBlockingQueue

new ArrayBlockingQueue<>(100)

这是一个有界队列,底层是数组,容量固定。

我的理解是:

  • 队列长度明确可控
  • 不容易因为无限堆积任务导致内存问题
  • 生产环境中比较常用

2)LinkedBlockingQueue

new LinkedBlockingQueue<>()

这是链表结构的阻塞队列。
如果不手动指定容量,默认容量非常大,接近 Integer.MAX_VALUE

我的理解是:

  • 好处是很能装
  • 风险是任务堆积太多时,容易撑爆内存
  • 因为队列太大,线程池也不容易扩容到 maximumPoolSize

这是一个很容易忽略的点。
因为很多人以为最大线程数一定会用上,其实未必。
如果队列特别大,任务会一直排队,线程池反而懒得扩容。


3)SynchronousQueue

new SynchronousQueue<>()

这个队列几乎不存储元素。
我可以把它理解成“直接交接”。

它的特点是:

  • 任务进来后不能真正排队
  • 必须立刻交给某个线程处理
  • 如果当前没有空闲线程接手,就倾向于新建线程

所以使用它的线程池,扩容通常会更激进。


我对 workQueue 的一个核心理解

workQueue 不只是存任务这么简单,它会直接影响线程池的扩容策略。

因为线程池的执行逻辑通常是:

  1. 先用核心线程
  2. 再尝试入队
  3. 队列满了才扩线程

所以:

  • 队列越大,越不容易创建更多线程
  • 队列越小,越容易触发扩容

也就是说,队列容量和线程数量是联动关系


3.6 threadFactory

threadFactory 表示 线程工厂,负责定义线程是怎么创建出来的。

我可以通过它控制:

  • 线程名称
  • 是否是守护线程
  • 线程优先级
  • 线程组
  • 未捕获异常处理器

例如自定义线程名:

ThreadFactory factory = r -> new Thread(r, “order-pool-thread”);

这样做的意义很大,因为排查问题时,我在日志、监控、线程 dump 里能一眼看出来这些线程是干什么的。

所以我对它的理解是:

threadFactory 决定线程的出生方式和身份信息


3.7 handler

handler 表示 拒绝策略

当下面三个条件同时满足时,线程池就处理不了了:

  1. 核心线程满了
  2. 队列满了
  3. 最大线程数也到了

这时候新提交的任务就只能被拒绝,拒绝时怎么处理,由 handler 决定。

JDK 常见的 4 种策略如下。

1)AbortPolicy

直接抛异常:

RejectedExecutionException

这是默认策略。
我的理解是,线程池明确告诉调用方:我处理不了了。


2)CallerRunsPolicy

谁提交任务,谁自己执行这个任务。

我的理解是:

  • 原来调用方只是负责提交
  • 现在线程池忙不过来了,就让调用方自己干
  • 这样会拖慢调用方的速度,形成一种天然的反压

3)DiscardPolicy

直接丢弃任务,不抛异常。

我的理解是:

  • 任务被悄悄扔掉了
  • 如果业务不允许丢任务,这个策略风险很大

4)DiscardOldestPolicy

丢弃队列里最早的任务,再尝试提交当前任务。

我的理解是:

  • 更偏向保留新任务
  • 适合某些对实时性要求更高的场景

4. 参数之间的关系

这几个参数不是独立的,我需要把它们联动起来理解。

4.1 corePoolSizemaximumPoolSize

  • corePoolSize 决定平时线程池至少维持多少个线程
  • maximumPoolSize 决定高峰期最多能扩到多少个线程

4.2 workQueuemaximumPoolSize

这个关系很关键。

只有在 队列满了 的前提下,线程池才会考虑继续创建非核心线程。
所以如果队列特别大,大多数任务都先排队了,线程池根本不急着扩容。

也就是说:

队列大小会影响最大线程数是否真的有机会生效


4.3 keepAliveTime 和非核心线程

高峰期线程池可能扩容出很多非核心线程。
任务高峰过去后,这些多出来的线程不能永久保留,所以要靠 keepAliveTime 控制它们逐步回收。


5. 线程池的执行流程

这一部分是线程池最核心的运行逻辑。

我重点看的是 execute(Runnable command) 这个方法。
当我提交一个任务时,线程池大体会按下面的顺序处理。


第一步:如果运行中的线程数小于 corePoolSize

线程池会优先创建一个核心线程来执行这个任务。

也就是说:

  • 不是先排队
  • 也不是先看最大线程数
  • 而是先尽量把核心线程补齐

我的理解是:

核心线程是线程池的基本工作班底,所以一开始优先建立它们


第二步:如果核心线程都满了,就尝试让任务进入 workQueue

这时线程池不会马上扩容,而是先把任务放进阻塞队列里等待。

我的理解是:

线程池的设计思路不是一上来就疯狂加线程,而是先让现有线程消化,同时让任务排队缓冲


第三步:如果队列也满了,再判断当前线程数是否小于 maximumPoolSize

如果还没达到最大线程数,线程池就会继续创建非核心线程来执行任务。

我的理解是:

  • 说明当前负载已经超过了“核心线程 + 队列缓冲”的承载能力
  • 这时线程池开始进入高峰扩容模式

第四步:如果队列满了,而且线程数也达到 maximumPoolSize

这时线程池彻底处理不过来了,就会执行拒绝策略 handler

也就是说,拒绝任务并不是第一时间发生的,而是在所有容量都耗尽之后才发生。


6. 我把整个执行流程写成一句话

当一个任务提交到线程池时:

  1. 先看核心线程是否已满
  2. 没满就创建核心线程执行任务
  3. 满了就尝试让任务进入阻塞队列
  4. 如果队列满了,再尝试创建非核心线程
  5. 如果线程数也达到最大值,就执行拒绝策略

7. 从源码角度看线程池内部是怎么跑的

如果再往底层看,我会发现线程池内部不是简单地“线程执行一次任务就结束”,而是通过 Worker 机制反复复用线程。


7.1 Worker 的作用

线程池内部会把线程封装成 Worker 对象。

我可以把一个 Worker 理解成:

  • 持有一个真正的线程
  • 负责不断取任务并执行
  • 在线程池中作为最小工作单元存在

所以线程池管理的不是一堆裸线程,而是一堆 Worker


7.2 线程为什么能复用

线程执行完一个任务后不会立刻销毁。
它会继续尝试从队列中获取下一个任务。

可以抽象成下面这种逻辑:

1
2
3
4
while (task != null || (task = getTask()) != null) {
task.run();
task = null;
}

我的理解是:

  • 第一次任务可能是提交时直接分配给它的
  • 后续任务可能是它从队列中不断取出来的
  • 只要还能取到任务,这个线程就会继续工作

这就是线程池“复用线程”的本质。


7.3 getTask() 的作用

getTask() 可以理解成线程去线程池里“领活”。

它的行为大概取决于:

  • 当前线程是否应该继续存活
  • 当前队列里有没有任务
  • 当前线程是不是非核心线程
  • 是否已经空闲超时

如果一个非核心线程长时间拿不到任务,就可能在这里返回 null,然后退出循环,最终被销毁。


8. 为什么线程池不是先扩到最大线程数再说

这是线程池设计里的一个重要思想。

如果一来任务就立刻疯狂建线程,会带来很多问题:

  1. 线程创建本身有成本
  2. 线程过多会导致上下文切换严重
  3. CPU 会在大量线程之间来回切换,真正干活的效率反而下降
  4. 线程太多还会占用更多内存资源

所以线程池采用的是一种更稳的策略:

  • 先建立核心线程
  • 再用队列缓冲
  • 真的扛不住了才扩容
  • 最后再拒绝

这是一种“优先稳态,再逐步弹性扩展”的设计。


9. 一个完整例子

import java.util.concurrent.*;

1
2
3
public class ThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
            2,  
            4,  
            60,  
            TimeUnit.SECONDS,  
            new ArrayBlockingQueue<>(2),  
            Executors.defaultThreadFactory(),  
            new ThreadPoolExecutor.AbortPolicy()  
    );  
1
2
3
4
5
6
7
8
9
10
11
for (int i = 1; i <= 7; i++) {
int taskId = i;
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务 " + taskId);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
1
2
3
        executor.shutdown();
}
}

10. 结合上面的例子理解执行过程

参数是:

  • 核心线程数 = 2
  • 最大线程数 = 4
  • 队列容量 = 2

假设 7 个任务几乎同时提交,执行过程可以这样理解:

第 1 个任务

当前线程数是 0,小于核心线程数 2
直接创建核心线程执行

第 2 个任务

当前线程数是 1,小于核心线程数 2
再创建一个核心线程执行

第 3 个任务

核心线程满了
尝试进入队列,成功

第 4 个任务

继续进入队列,成功
此时队列也满了

第 5 个任务

核心线程满,队列满
当前线程总数 2,小于最大线程数 4
创建一个非核心线程执行

第 6 个任务

同理,再创建一个非核心线程执行
此时线程总数达到 4

第 7 个任务

核心线程满
队列满
最大线程数也满
执行拒绝策略


11. 我对线程池整体运行方式的总结

我可以把线程池理解成一个四层缓冲结构:

第一层:核心线程

优先直接执行任务

第二层:阻塞队列

核心线程忙时,任务先排队等待

第三层:非核心线程

队列满了以后,线程池再临时扩容

第四层:拒绝策略

所有资源都用尽后,对新任务做最终处理


12. 一段我自己会放进笔记里的总结

线程池的本质是复用线程和控制并发。
ThreadPoolExecutor 的核心参数包括核心线程数、最大线程数、空闲存活时间、时间单位、任务队列、线程工厂和拒绝策略。
任务提交后,线程池会先尝试创建核心线程执行;核心线程满了,任务进入阻塞队列;队列满了,再创建非核心线程;如果最大线程数也达到上限,就执行拒绝策略。
线程池内部通过 Worker 持有线程,并让线程不断从队列中获取任务执行,因此线程可以被反复复用,而不是执行一次就销毁。


13. 极简版笔记

参数

  • corePoolSize:核心线程数
  • maximumPoolSize:最大线程数
  • keepAliveTime:非核心线程空闲存活时间
  • unit:时间单位
  • workQueue:任务阻塞队列
  • threadFactory:线程创建工厂
  • handler:拒绝策略

执行流程

  • 线程数小于核心线程数,直接创建核心线程执行
  • 核心线程满了,任务进入队列
  • 队列满了且线程数小于最大线程数,创建非核心线程执行
  • 队列和线程都满了,执行拒绝策略