线程池
为什么需要线程池?
-
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
-
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
-
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池重要参数
corePoolSize
: 任务队列未达到队列容量时,最大可以同时运行的线程数量
maximumPoolSize
: 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数
workQueue
: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中
ThreadPoolExecutor
其他常见参数 :
keepAliveTime
:线程池中的线程数量大于corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
才会被回收销毁unit
:keepAliveTime
参数的时间单位threadFactory
:用来创建新线程handler
:拒绝策略AbortPolicy
:抛异常DiscardPolicy
:直接丢弃DiscardOldestPolicy
:丢弃任务队列中最早的未处理任务,然后将新任务加入队列;CallerRunsPolicy
:由提交任务的线程(通常是主线程)来执行这个任务
自定义线程池
ThreadPoolExecutor threadPool = ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
ThreadPoolExecutor
的某些常量定义
public class ThreadPoolExecutor extends AbstractExecutorService {
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
//线程的状态
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
private final HashSet<Worker> workers = new HashSet<Worker>();
}
主线程池控制状态 ctl
是一个原子整数(AtomicInteger
),它打包了两个概念字段:
workerCount
:记录的理论上应该存在的工作线程数,而线程池中实际活跃的线程数量(pool size
),是由内部workers
集合来维护的,实际活跃线程数可能暂时与该值不同(后面提交任务会提到原因)runState
:表示线程池的运行状态,如运行中、关闭中等。
COUNT_BITS = Integer.SIZE - 3
;线程池的状态占用Integer
的高三位
CAPACITY = (1 << COUNT_BITS) - 1
;workerCount
的上限限制为 (2^29) - 1
(大约 5 亿个线程);如果需要支持更多线程,可以将 ctl
改为 AtomicLong
,同时调整相关的位移和掩码常量。
runState
(运行状态):
- RUNNING(运行中):接受新任务,并处理队列中的任务;
- SHUTDOWN(关闭中):不再接受新任务,但继续处理队列中的任务;
- STOP(停止):不接受新任务,也不处理队列中的任务,同时中断正在执行的任务;
- TIDYING(整理中):所有任务已终止,
workerCount
为 0,进入该状态的线程池将调用terminated()
钩子方法进入TERMINATED
状态 - TERMINATED(已终止):
terminated()
执行完毕。
状态转变的流程如下:
RUNNING
→SHUTDOWN
:调用shutdown()
或在finalize()
中隐式触发;RUNNING
或SHUTDOWN
→STOP
:调用shutdownNow()
;SHUTDOWN
→TIDYING
:队列和线程池都为空时;STOP
→TIDYING
:线程池为空时;TIDYING
→TERMINATED
:terminated()
方法执行完毕。
线程池提交任务的两种方式
execute()
方法
只能提交 Runnable
,没有返回值。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//第一步
if (workerCountOf(c) < corePoolSize) {
//参数true:if true use corePoolSize as bound, else maximumPoolSize.
if (addWorker(command, true))
return;// 失败时更新 ctl 再进入第二步
c = ctl.get();
}
//第二步
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}//第三步
else if (!addWorker(command, false))
reject(command);
}
addWorker(Runnable firstTask, boolean core)
:该方法用于检查当前线程池状态以及线程数量是否允许添加新的工作线程
-
firstTask
:新线程应首先运行的任务(如果没有则为null
)。 -
core
:布尔值,表示是否使用corePoolSize
(核心线程数)作为上限判断。
在addWorker
方法中,先修改workerCount
再修改workers
,所以实际活跃线程数可能暂时与workerCount
不同
compareAndIncrementWorkerCount(c)//workerCount+1
...
w = new Worker(firstTask);
...
workers.add(w);//工作线程集合中加入一个线程
execute()
整个流程分为三步:
第一步:worker 数少于 corePoolSize,尝试创建新线程执行任务
第二步:任务尝试入队后,需要二次检查线程池状态
-
workQueue.offer(command):任务成功入队。
-
然后需要二次检查 ctl 状态(recheck):
- 如果状态已不是
RUNNING
,说明shutdown()
或shutdownNow()
被调用了,就要回滚任务(remove
)并拒绝。 - 否则,如果当前没有工作线程(可能任务入队时线程池空了),那就得启动一个线程去处理队列任务(
addWorker(null, false)
)。
- 如果状态已不是
第三步:入队失败时,尝试再新建一个线程;否则拒绝任务
可结合下面这个图来理解
submit()
方法
适用于 Runnable
或 Callable
任务,依赖execute()
执行任务
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
submit
它会将传进来的任务封装成RunnableFuture
,然后将Future
返回出去,调用者可以通过get
方法获取返回结果
Worker
线程管理
Worker
线程
线程池为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程Worker
。
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
final Thread thread; // 真正运行任务的线程对象
Runnable firstTask; // 该 Worker 启动时要执行的第一个任务(构造时传入)
volatile long completedTasks; // 该 Worker 运行的任务数量(用于统计)
}
firstTask
用它来保存传入的第一个任务,这个任务可以有也可以为null
。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是null
,那么就需要创建一个线程去执行任务列表(workQueue
)中的任务,也就是非核心线程的创建。
Worker
执行任务的模型如下图所示:
Worker
线程增加
private boolean addWorker(Runnable firstTask, boolean core) {
......
w = new Worker(firstTask);
final Thread t = w.thread;
......
workers.add(w);
...
workerAdded = true;
........
if (workerAdded) {
t.start();
workerStarted = true;
}
.......
}
该方法的功能就是增加一个线程,该方法不考虑线程池是在哪个阶段增加的该线程,这个分配线程的策略是在上个步骤完成的,该步骤仅仅完成增加线程,并使它运行,最后返回是否成功这个结果。
启动worker
的过程为:
Worker
构造函数中,会创建线程对象 thread
Worker(Runnable firstTask) {
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this); // this 实现了 Runnable
}
这里 this
就是 Worker 实例。
因为 Worker 实现了 Runnable
接口,所以当执行 t.start()
时,底层会调用:
Worker.run()
而 Worker 的 run()
方法中,会进一步调用线程池的主循环方法:
public void run() {
runWorker(this);
}
最终由 runWorker(Worker w)
来不断从队列中获取任务并执行。
Worker
线程执行任务
worker
调用了runWorker
方法来执行任务,通过getTask()
从工作队列中取任务
while (task != null || (task = getTask()) != null)
Worker
线程回收
线程池使用一张Hash
表去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。线程池中线程的销毁依赖JVM自动的回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止这部分线程被JVM回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可。
private final HashSet<Worker> workers = new HashSet<Worker>();
这个时候重要的就是如何判断线程是否在运行。
Worker
被创建出来后,就会不断地进行轮询,然后通过getTask()
获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务。当Worker
无法获取到任务,也就是获取的任务为空时,循环会结束,Worker
会主动消除自身在线程池内的引用。
线程回收的工作是在processWorkerExit
方法完成的。
try {
while (task != null || (task = getTask()) != null) {
.......
} finally {
processWorkerExit(w, completedAbruptly);
}
在processWorkerExit
中将worker
引用移出wokers
另外,Worker
是通过继承AQS
来实现独占锁这个功能,线程池在执行shutdown
方法或tryTerminate
方法时会调用interruptIdleWorkers
方法来中断空闲的线程,interruptIdleWorkers
方法会使用tryLock
方法来判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。
线程池拒绝策略的使用场景
- AbortPolicy(默认策略):适合重要性高且时效性高的场景,必须确保关键任务(如支付场景、订单提交)正常执行,拒绝后需要快速失败并通知调用方处理异常
- CallerRunsPolicy(调用者执行策略):适合重要性高 + 时效性低,允许任务延迟执行,但不能丢弃(如异步日志写入、非核心业务通知)
- DiscardPolicy(直接丢弃策略):适合重要性低且时效性低的场景,非关键任务(如统计数据采集),允许少量丢失
- DiscardOldestPolicy(丢弃最旧任务策略):适合重要性低且时效性高的场景,新任务比旧任务更重要(如实时状态更新、动态配置刷新),保留最新数据。
线程池的几种创建方法
-
固定大小线程池:使用
newFixedThreadPool()
,线程池大小固定,适用于处理稳定的任务数量。ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
-
可缓存线程池:使用
newCachedThreadPool()
,适用于处理大量短时间的异步任务。newCachedThreadPool()
创建一个大小不固定的线程池,根据需要创建新的线程,空闲线程超过 60 秒则会被终止。ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
-
单线程线程池:使用
newSingleThreadExecutor()
,适用于顺序执行任务的场景。ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
-
定时任务线程池:使用
newScheduledThreadPool()
,适用于需要定时或周期性任务的场景。ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
-
自定义线程池:通过
ThreadPoolExecutor
灵活控制线程池的参数,适用于需要对线程池进行精细化管理的场景。
他们之间的关系可以看下面这张图
线程池的关闭方式
shutdown
:使用这个方法之后,无法提交新的任务进来,线程池会继续工作,将手头的任务执行完再停止
executor.shutdown();
shutdownNow
:这种停止方式就比较粗暴了,立即中断所有正在执行的线程,返回尚未开始执行的任务列表(List<Runnable>
),通常用于异常退出、紧急停机
List<Runnable> notExecutedTasks = executor.shutdownNow();
线程池的大小如何设定?
CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1。
I/O 密集型任务(2N):CPU 核心数的两倍
1.1 CPU 密集型任务
- 定义:CPU 密集型任务主要消耗的是 CPU 资源,比如数学计算、图像处理等。
- 推荐线程数:通常情况下,线程数应该设置为 CPU 核心数 + 1 或 CPU 核心数,这样可以让 CPU 保持高负载,同时减少上下文切换的开销。
- 公式:
线程池大小 = CPU 核心数 + 1
。 - 其中
+1
是为了应对偶尔的 I/O 操作或任务挂起情况,这样即使一个线程暂时被挂起,CPU 依然有线程可以执行其他任务。
- 公式:
1.2 I/O 密集型任务
- 定义:I/O 密集型任务通常会涉及大量的 I/O 操作,比如网络请求、文件读写等,这些任务会导致线程在等待 I/O 操作完成时空闲。
- 推荐线程数:线程池的大小可以设置为 (CPU 核心数 × 2) 或 (CPU 核心数 × 2) ~ (CPU 核心数 × 4)。I/O 密集型任务在执行过程中会有大量的等待时间,因此可以创建更多的线程来并发处理任务。
- 公式:
线程池大小 = CPU 核心数 × (1 + 任务等待时间与任务计算时间之比)
。 - 该公式通过考虑任务的计算时间和等待时间的比例来调整线程池的大小。
- 公式:
笔者也认为不用太纠结这个固定的方式,可以灵活应对
这篇文章写的还可以:https://mp.weixin.qq.com/s/IdJhR7ANn3iCno1CVsF4WA
思考
为什么先要用corePoolSize
核心线程,然后当核心线程处理不过来时将异步任务先放到workQueue
中,而不是直接开maximumPoolSize
的线程数继续处理应急任务呢?
假设任务处理不过来之后,直接创建maximumPoolSize个线程处理任务,会存在以下问题:
- 线程数暴增导致资源耗尽:如果一波高并发任务突然涌入,线程池马上创建大量线程,会导致内存压力急剧增加,出现 频繁 GC,甚至 OOM(
OutOfMemoryError
)。工作队列(如LinkedBlockingQueue
)可以暂时缓存任务,让系统更平稳处理流量波动。它提供了一种“削峰填谷”的机制。 - 非核心线程(core 以外的线程)需要一定时间才会被回收(受
keepAliveTime
控制)。在此期间,系统依然维持大量空闲线程,占用资源。
最大线程数通常用于应对短时间的突发负载,他是一种应急措施。
参考链接:
https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html
https://mp.weixin.qq.com/s/JHgRDz7qGAiqpHq5ToL85Q