前言
老生常谈的线程池,我们来重新梳理一遍。
Executor 简介
Java 中的线程池都是以 Executor 为根基。Executor是执行提交的Runnable任务的对象。该接口提供了一种将任务提交与运行解耦的机制,其中包括隐藏了线程使用、调度等的细节。
在HotSpot VM的线程模型中,Java线程(java.lang.Thread)被一对一映射为本地操作系统线程。Java线程启动时会创建一个本地操作系统线程;当该Java线程终止时,这个操作系统线程也会被回收。操作系统会调度所有线程并将它们分配给可用的CPU。
Doug Lea 大佬在源码注释中告诉我们,通常要使用Executor而不是显式创建线程。
典型用法如下:
Executor executor = anExecutor;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
我们日常最常用的是 ThreadPoolExecutor
ThreadPoolExecutor 简介
一个ExecutorService ,它使用可能的多个池线程之一执行每个提交的任务,通常使用Executors工厂方法进行配置。
线程池解决了两个不同的问题:
- 减少了每个任务的调用开销,它们通常在执行大量异步任务时提供改进的性能
- 提供了一种限制和管理资源的方法,包括执行一组线程任务时消耗的线程。
此外,每个 ThreadPoolExecutor 还维护了一些基本的统计信息,例如完成任务的数量。
为了在广泛的上下文中有用,这个类提供了许多可调整的参数和 hooks 。
此外,还通过 Executors 工厂类,提供一些常见的使用场景预配置。
- ExecutorsnewCachedThreadPool - 无界线程池,具有自动线程回收功能
- ExecutorsnewFixedThreadPool - 固定大小的线程池
- ExecutorsnewSingleThreadExecutor - 单线程线程池
- newScheduledThreadPool - 定时及周期性任务
此处 《Java 开发手册(黄山版)》一、编程规约 (七) 并发处理
4.【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方
式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
3)ScheduledThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
虽说《Java 开发手册(黄山版)》中规定了不允许使用 Executors 创建线程池,但是还是看实际情况吧,确定业务达不到那么多请求,使用起来也无妨。
毕竟我在看 Nacos 源码,发现里面也有使用Executors的情况存在。
ThreadPoolExecutor 核心参数
画了一张比较简略的图,表示大致的意思。
这是一个参数比较全的 ThreadPoolExecutor 构造器,通过这个构造器,我们可以对线程池的配置一探究竟。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
{...}
-
核心线程数与最大线程数 ThreadPoolExecutor 将根据 corePoolSize 和 maximumPoolSize 自动调整线程池大小。 当在方法execute(Runnable)中提交了一个新任务,并且运行的线程少于 corePoolSize 时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求。 如果运行的线程数多于 corePoolSize 但少于 maximumPoolSize,则仅当队列已满时才会创建新线程。
- 通过将 corePoolSize 和 maximumPoolSize 设置为相同,可以创建一个固定大小的线程池。
- 通过将 maximumPoolSize 设置为基本上无界的值,例如Integer.MAX_VALUE ,可以允许池容纳任意数量的并发任务。 核心和最大池大小可以在构造时设置,也可以使用
setCorePoolSize()和setMaximumPoolSize()动态更改。
- 保活时间 即非核心线程,无任务可处理时,最长存活时间。
- 线程工厂
ThreadPoolExecutor使用 ThreadFactory 创建新线程。 如果没有另外指定,则使用 ExecutorsdefaultThreadFactory,它创建的线程都处于相同的 ThreadGroup 并具有相同的优先级和非守护进程状态。 通过提供不同的 ThreadFactory,可以更改线程的名称、线程组、优先级、守护进程状态等。
-
任务队列
任何BlockingQueue都可以用来传输和保存提交的任务。
使用队列与线程池大小相关:
- 如果运行的线程少于 corePoolSize,则 Executor 添加新线程而不加入队列。
- 如果达到 corePoolSize 或有更多线程正在运行,Executor 排队请求而不添加新线程。
- 如果请求无法排队,则会创建一个新线程。如果超过 maximumPoolSize,在这种情况下,该任务将被拒绝。
排队策略有以下三种:
- 直接交接。 默认工作队列是 SynchronousQueue ,它把任务直接交给线程处理。如果没有立即可用的线程来运行任务,将构造一个新线程。
- 无界队列。 所有 corePoolSize 线程都在处理任务时,新任务将在无界队列中等待。 当线程池使用无界队列时,不会创建超过 corePoolSize 个线程,即最大线程数参数无效。
- 有界队列。 有界队列(例如ArrayBlockingQueue )在与 maximumPoolSizes 一起使用时,可以控制服务器压力。
队列大小和线程池大小可以相互配合:使用大队列小池可以减少 CPU 使用率、上下文切换开销,但会导致吞吐量降低。使用小队列大池, CPU 的利用率更高,但可能会有频繁线程上下文切换,这也会降低吞吐量。 所以,在使用时,根据实际情况来配置队列大小和线程池大小是非常有必要的。
-
拒绝策略
当 Executor 关闭时,或者 有界队列饱和时,在方法
execute(Runnable)中提交的新任务将被拒绝。无论哪种情况, execute方法都会调用RejectedExecutionHandler.rejectedExecution(Runnable, ThreadPoolExecutor)方法。提供了四种处理策略:
- 默认ThreadPoolExecutor.AbortPolicy - 抛出
RejectedExecutionException异常。 - ThreadPoolExecutor.CallerRunsPolicy - 调用 execute 线程(Main)运行任务。这是一种简单的反馈控制机制,可以减慢提交新任务的速度。
- ThreadPoolExecutor.DiscardPolicy - 丢弃任务。
- ThreadPoolExecutor.DiscardOldestPolicy - 如果 executor 没有关闭,则丢弃工作队列头部的任务,然后重试执行(可能再次失败,导致重复此操作。)
- 默认ThreadPoolExecutor.AbortPolicy - 抛出
此外,可以自定义 RejectedExecutionHandler 子类。
ThreadPoolExecutor 的其他特性
-
核心线程预热 默认情况下,即使是核心线程也仅在新任务到达时才最初创建和启动,但可以使用方法
prestartCoreThreadp启动一个core thread 或者prestartAllCoreThreads启动所有核心线程。 -
Hooks 方法
beforeExecute(Thread, Runnable)和afterExecute(Runnable, Throwable)方法,这些方法在执行每个任务之前和之后调用。可用于操纵执行环境;例如,重新初始化 ThreadLocals、收集统计信息或添加日志条目。此外,可以重写方法terminated以执行任何特殊处理,Executor 完全终止时调用。
如果钩子或回调方法抛出异常,内部工作线程可能会依次失败并终止。
-
获取队列 方法
getQueue()允许访问工作队列进行监视和调试。强烈建议不要将此方法用于任何其他目的。
ThreadPoolExecutor 的执行过程
execute(Runnable command)
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 当线程数量少于 corePoolSize时,每次都新创建线程执行任务。
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 持有线程数量大于corePoolSize,任务入队。
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);
}
Worker 与 addWorker
通过 execute 方法,我们可以看到:ThreadPoolExcutor 会将 Runable 封装成为一个 Worker,然后添加 Worker 到 ThreadPoolExcuter 中。Worker 本身还继承了 AQS 、实现了 Runnable。
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
// worker 持有的线程。
final Thread thread;
// worker 持有的第一个任务
Runnable firstTask;
// worker 完成的任务数量
volatile long completedTasks;
// ......
// 这里需要注意一下,创建 Work 的时候,会创建一个新线程。并且把自己本身作为任务,传递给线程。
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
}
这里先提前科普一个小知识
break retry 跳到retry处,且不再进入循环 continue retry 跳到retry处,且再次进入循环
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 前置状态判断
if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty()))
return false;
// 这一部分,主要是判断线程池数量是否满足。
for (;;) {
int wc = workerCountOf(c);
// 如果线程总数超过了容量,或者超过了(corePoolSize,maxPoolSize),直接返回false。
if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 开始增加线程总数数量。
if (compareAndIncrementWorkerCount(c))
// 如果增加成功了,就中断自旋。
break retry;
c = ctl.get(); // Re-read ctl
// 线程线程池状态,与刚进入方法时,是否一致。不一致就重新进入循环自旋。
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 创建工作线程
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
// 获取线程池实例的全局锁。防止并发添加线程,导致线程数量超过预期值。
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 再次检查线程池状态
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) throw new IllegalThreadStateException();
// 添加到工作线程中。
workers.add(w);
int s = workers.size();
if (s > largestPoolSize) largestPoolSize = s;
workerAdded = true;
}
} finally {
// 释放线程池全局锁
mainLock.unlock();
}
if (workerAdded) {
// 启动线程。这里实际上也就是调用 Worker#run()
t.start();
workerStarted = true;
}
}
} finally {
// ... 处理添加启动线程失败的逻辑...
}
return workerStarted;
}
runWorker
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// 这里开始处理 task 了。
// 如果指定有 firshTask,先处理 firshTask。
// 否则,从 阻塞队列中获取 Task 执行,一直到队列中没有任务。
while (task != null || (task = getTask()) != null) {
w.lock();
if (
(runStateAtLeast(ctl.get(), STOP)
|| (
Thread.interrupted() && runStateAtLeast(ctl.get(), STOP)
)
) && !wt.isInterrupted())
wt.interrupt();
try {
// 处理任务前做些什么。
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} // ... catch
} finally {
// 处理任务之后做些什么。
afterExecute(task, thrown);
}
} finally {
task = null;
// 任务数量 ++
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
// 如果报错了,或者因为没有 Task 了。
// 这个线程就要结束了,处理身后事。
processWorkerExit(w, completedAbruptly);
}
}
getTask()
private Runnable getTask() {
// 从 workQueue 中获取任务是否超时。
boolean timedOut = false;
// 开始自旋
for (;;) {
// 状态检查
int c = ctl.get();
int rs = runStateOf(c);
// 如果线程池即将关闭,队列中没任务了。
// 直接返回null。
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// 判断是否需要淘汰工作线程
// 是否非常驻的非核心线程
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
// 如果非核心线程不需要常驻,通过带有超时时间的获取方法,获取任务。
// 否则调用阻塞获取。
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
简略地说就是,线程被创建出来后,处理完指定给自己的第一个任务后,就去阻塞队列中轮询获取下一个任务进行处理。如果一直有任务的话,也就一直处理任务,不会销毁线程了。如果阻塞队中没有任务了,就根据设置决定是否从 Set中,移除线程。
小结
这一篇中,我们主要重新复习 Java 中的线程池,包括了:
- 基本概念。
- 核心配置项。
- 其他特性例 - 如核心线程预热、钩子方法之类。
- 核心的运行方法。
但,还有一些知识面没有覆盖到,例如 BlockingQueue 阻塞队列等等。
我们后面继续~
参考资料
- Java线程池实现原理及其在美团业务中的实践 by 美团技术团队
- 《Java 并发编程的艺术》by 方腾飞
- 《深入浅出 Java 多线程》by redspider 社区
- Java Doc - java.util.concurrent.ThreadPoolExecutor by Doug Lea