Java 线程池详解
概述
Java 中的线程池是运行场景最多的并发框架,合理使用线程池能够带来三个好处:
- 降低资源消耗。通过重复利用已有的线程降低线程创建和销毁造成的消耗
- 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行
- 提高线程可管理性。线程是稀缺资源,使用线程池进行统一分配、调优和监控,可以降低资源消耗,提高系统稳定性
线程池的实现原理
从图中可以看到,当提交一个新任务到线程池时,线程池的处理流程如下:
- 线程池判断核心线程池里的线程是否都在执行任务,如果不是,创建一个新的工作线程执行任务,否则进入下一流程
- 线程池判断工作队列是否已满,如果工作队列没有满,将新提交的任务存储在工作队列中,否则进入下一流程
- 线程池判断线程池里的线程是否都处于工作状态,如果没有,创建一个新的工作线程执行任务,否则交给饱和策略来处理这个任务
使用线程池
1. 创建线程池
我们可以通过 ThreadPoolExecutor 来创建一个线程池
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
创建一个线程需要输入几个参数,如下:
-
corePoolSize(线程池的基本大小)
当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即时其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建
-
maximumPoolSize(线程池最大数量)
线程池允许创建的最大线程数,如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用无界阻塞队列做任务队列,则这个参数没有什么效果
-
keepAliveTime(线程活动保持时间)
线程池的工作线程空闲后,保持存活的时间。如果任务很多,并且每个任务的执行时间都比较短,可以调大时间,提高线程利用率
-
unit(线程保持活动时间的单位)
可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微妙(MICROSECONDS)和纳秒(NANOSECONDS)
-
workQueue(任务队列)
用于保存等到执行的任务的阻塞队列,可以选择以下几个阻塞队列:
-
ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序
-
LinkedBlockingQueue
一个基于链表结构的阻塞队列,此队列按 FIFO 排序元素,吞吐量通常高于 ArrayBlockingQueue
-
SynchronousQueue
一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一致处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue
-
PriorityBlockingQueue
一个具有优先级的无界阻塞队列
-
-
threadFactory
用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字
-
handler(饱和策略)
当任务和线程池都满了,说明线程池处于饱和状态,必须采取一种策略处理提交的新任务。在 JDK5 中线程池框架提供了四种策略,也可以根据需要实现 RejectedExecutionHandler 接口自定义策略:
- AbortPolicy:直接抛出异常,默认采取这种策略,一般出现异常时,可以提示稍后再试,或者把未执行的任务记录下来,等到适当时机再次执行
- CallerRunsPolicy:使用调用者所在线程来运行任务,保证所有任务都能执行,但如果并发量过大会有风险
- DiscardOldestPolicy:丢弃队列最老(也就是最近将要执行)的一个任务,并执行当前任务,适用于需要淘汰等待时间最长任务的场景
- DiscardPolicy:不处理,丢弃掉,一般不使用,除非需要执行的任务不重要,丢弃了也没事,或者能把丢弃的任务几楼下来,等到适当时机再次执行
如何合理设置线程池参数,需要根据执行的任务类型来确定,一般来说任务分 CPU 密集型和 IO 密集型两种:
- CPU 密集型:业务逻辑复杂的任务,IO 较少,几乎全程没有阻塞
- IO 密集型:业务逻辑简单的任务,但 IO 频繁
查看机器的 CPU 核数
System.out.println(Runtime.getRuntime().availableProcessors());
对于 CPU 密集型任务,假设 CPU 核数为 N,可以设置 corePoolSize = N,因为 CPU 密集型任务对 CPU 的切换调度较少
对于 IO 密集型任务,假设 CPU 核数为 N,可以设置 corePoolSize = 2*N,因为线程在等待 IO 时会释放 CPU
maxPoolSize,最大线程数,一般设置成和 corePoolSize 一样,减少创建线程的开销
keepAliveTime,空闲线程的存活时间,超过这个时间无任务执行的线程将被终止,直到线程数回到 corePoolSize。对于 CPU 密集型任务,可以设置较短的存活时间,减少资源浪费。对于 IO 密集型任务,可以设置较长的存活时间,保持线程池的响应能力
handler,饱和策略,根据业务场景选择合适的饱和策略。如果不能丢失任务,可以选择CallerRunsPolicy。如果接受任务被丢弃,可以选择 DiscardOldestPolicy 或 DiscardPolicy
workQueue,任务队列。如果使用无界队列,当任务耗时较长时,可能会导致大量任务在队列堆积,最终导致 OOM。使用有界队列可以防止资源耗尽,容量大小应根据任务生产速率和执行速率来设定,防止队列过早填满导致线程池迅速扩张。如果希望任务不等待直接移交给工作线程,可使用 SynchronousQueue,要将一个任务放入 SynchronousQueue,必须有另一个线程正在等待接收这个元素,只有在使用无界线程池(maximumPoolSize = Integer.MAX_VALUE)或者有饱和策略时才建议使用该队列
2. 向线程池提交任务
可以使用 execute() 和 submit() 方法向线程池提交任务
-
execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功
threadsPool.execute(new Runnable() { @Override public void run() { //... } })
-
submit() 方法用于提交需要返回值的任务,线程池会返回一个 future 对象,通过这个对象可以判断任务是否执行成功
Future<Object> future = executor.submit(hasReturnValueTask); try { Object s = future.get(); } catch(InterruptedException e) { // 处理中断异常 } catch(ExecutionException e) { // 处理无法执行任务异常 } finally { // 关闭线程池 executor.shutdown(); }
3. 关闭线程池
可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池,它们的原理是遍历线程池中的工作线程,逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止
shutdown 方法和 shutdownNow 方法存在一定的区别:
- shutdownNow 方法首先将线程池状态设置成 STOP,不接收新任务,不处理已添加的任务,且会中断正在执行任务的线程
- shutdown 方法只是将线程池状态设置成 SHUTDOWN 状态,不接收新任务,但可以处理已添加的任务,等待正在执行任务的线程结束
只要调用了这两个关闭方法中的任意一个,isShutdown 方法就会返回 true,当所有任务都已关闭,才表示线程池关闭成功,这时调用 isTerminaed 方法会返回 true。至于应该采用哪种方法关闭线程池,应该由提交到线程池的任务特性决定,通常调用 shutdown 方法关闭线程池,如果任务不一定要执行完成,可以调用 shutdownNow 方法
线程池原理
由线程池的使用流程,我们可以得知,向线程池提交任务的方法是 execute 方法,因此我们首先从该方法入手:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
// ctl.get 的获取值能用来判断线程池状态和线程数
int c = ctl.get();
// 1.1 workerCountOf 方法用于获取线程池中线程数
if (workerCountOf(c) < corePoolSize) {
// 1.2 线程池中线程数小于核心线程数,尝试创建核心线程执行任务
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2.1 到此处说明线程池中线程数大于核心线程数或者创建线程失败
// 2.1 如果线程是运行状态并且可以使用 offer 将任务加入阻塞队列
// 2.2 offer 是非阻塞操作
if (isRunning(c) && workQueue.offer(command)) {
// 2.3 重新检查线程池状态,因为上次检测后线程池状态可能发生改变
int recheck = ctl.get();
// 2.4 如果非运行状态就移除任务并执行拒绝策略
if (! isRunning(recheck) && remove(command))
reject(command);
// 2.5 如果是运行状态,并且线程数是0,则创建线程
else if (workerCountOf(recheck) == 0)
// 2.6 线程数是0,则创建非核心线程,且不指定首次执行任务
// 2.7因为此时任务已经加入阻塞队列,只需要等待线程获取执行即可
addWorker(null, false);
}
// 3.1 阻塞队列已满,创建非核心线程执行任务
else if (!addWorker(command, false))
// 3.2 如果失败,则执行拒绝策略
reject(command);
}
接下来看 execute 方法中创建线程的 addWoker 方法,addWoker 方法承担了核心线程和非核心线程的创建,通过一个 boolean 参数 core 来区分是创建核心线程还是非核心线程
private boolean addWorker(Runnable firstTask, boolean core) {
// 这里做了一个 retry 标记,相当于 goto
retry:
for (int c = ctl.get();;) {
// 如果线程池处于关闭状态则不创建线程
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP) || firstTask != null || workQueue.isEmpty()))
return false;
for (;;) {
// 1.1 根据 core 来确定创建最大线程数,超过最大值则创建线程失败,
// 1.2 注意这里的最大值可能有三个 corePoolSize、maximumPoolSize 和线程池线程的最大容量 CAPACITY
if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 1.3 通过 CAS 将线程数 +1,如果成功则跳出循环,执行 2.1 的逻辑
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get();
// 1.4 线程池的状态发生了改变,退回 retry 重新执行
if (runStateAtLeast(c, SHUTDOWN))
continue retry;
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 2.1 实例化一个 Worker,内部封装了线程
w = new Worker(firstTask);
// 2.2 取出新建的线程
final Thread t = w.thread;
if (t != null) {
// 2.3 加锁保证线程安全
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
int rs = runStateOf(ctl.get());
// 2.4 拿到锁后重新检查线程池状态,只有处于 RUNNING(RUNNING 值小于 SHUTDOWN)
// 或者 SHUTDOWN 并且 firstTask==null 才会创建线程
if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {
// 2.5 线程已经启动则抛出异常
if (t.isAlive())
throw new IllegalThreadStateException();
// 2.6 将线程加入线程队列,这里的 workers 是一个HashSet
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
// 2.7 开启线程执行任务
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
至此,线程创建成功并开始执行任务,执行完后线程的生命周期就结束了,那么线程池如何保证 Worker 执行完任务后仍然不结束呢?当线程空闲超时或者关闭线程池又是怎样进行线程回收的呢?其中的实现逻辑就在 Worker 当中
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
// 执行任务的线程
final Thread thread;
// 初始化 Worker 传进来的任务,可能为 null,如果不为空则立即执行这个 task
Runnable firstTask;
Worker(Runnable firstTask) {
setState(-1);
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
// 线程的真正执行逻辑
public void run() {
runWorker(this);
}
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
// 取出 Worker 中的任务,可能为空
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// task 不为 null 或者阻塞队列中有任务,通过循环不断的从阻塞队列中取出任务执行
while (task != null || (task = getTask()) != null) {
w.lock();
// ...
try {
// 任务执行前的 hook 点
beforeExecute(wt, task);
try {
// 执行任务
task.run();
// 任务执行后的 hook 点
afterExecute(task, null);
} catch (Throwable ex) {
afterExecute(task, ex);
throw ex;
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
// 超时没有取到任务,则回收空闲超时的线程
processWorkerExit(w, completedAbruptly);
}
}
// ...
}
runWorker 方法的核心逻辑就是不断通过 getTask 方法从阻塞队列中获取任务并执行,因此线程的保活逻辑也在该方法中
private Runnable getTask() {
boolean timedOut = false;
for (;;) {
int c = ctl.get();
// ...
// 如果配置了 allowCoreThreadTimeOut == true
// 或者线程池中的线程数大于核心线程数,则 timed = true,表示开启指定线程超时后被回收
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// ...
try {
// 取出阻塞队列中的任务,如果 timed = true,则会调用阻塞队列的 poll 方法,
// 并设置超时时间为 keepAliveTime,如果超时没有取到任务则会返回 null
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
timed 为 true 的情况下调用阻塞队列的 poll 方法,并传入超时时间为 keepAliveTime,poll 方法是一个阻塞方法,在没有任务时会阻塞,如果在 keepAliveTime 时间内没有获取到任务就会返回 null,runWorker 方法的循环结束,线程也就结束被回收了
timed 为 false 的情况下调用阻塞队列的 take 方法,take 方法也是一个阻塞方法,在没有任务时会一直阻塞等待,这个线程也就一直保活了
综合以上,我们可以得出结论:并不是线程池的所有线程都需要一直保活,只有核心线程需要保活,非核心线程就不需要保活,超过设置的空闲时间就会被回收
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战