线程池原理
下面我将围绕这几个问题,来讨论一下线程池。
- 线程池是什么?
- 为什么使用线程池,或者说使用线程池的好处是什么?
- 线程池怎么使用?
- 线程池的原理是什么,它怎么做到重复利用线程的?
1. 是什么
线程池(Thread Pool)是一种基于池化思想的管理线程的工具,它内部维护了多个线程,目的是能重复利用线程,控制并发量,降低线程创建及销毁的资源消耗,提升程序稳定性。
2. 为什么
使用线程池的好处:
- 降低资源消耗:重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
线程池解决的核心问题就是资源管理问题,在并发场景下,系统不能够确定在任意时刻,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:
- 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能非常巨大。
- 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
- 系统无法合理管理内部的资源分布,会降低系统的稳定性。
线程池这种基于池化思想的技术就是为了解决这类问题。
3. 怎么用
线程池的的核心实现类是ThreadPoolExecutor
,调用execute
或者submit
方法即可开启一个子任务。
public class ThreadPoolTest {
private static ThreadPoolExecutor poolExecutor =
new ThreadPoolExecutor(1, 1, 5, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1));
public static void main(String[] args) throws ExecutionException, InterruptedException {
Runnable runnableTask = () -> System.out.println("runnable task end");
poolExecutor.execute(runnableTask);
Callable<String> callableTask = () -> "callable task end";
Future<String> future = poolExecutor.submit(callableTask);
System.out.println(future.get());
}
}
ThreadPoolExecutor
的核心构造器有7个参数,我们来分析一下每个参数的含义:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
// 省略...
}
corePoolSize
:线程池的核心线程数。线程池中的线程数小于corePoolSize
时,直接创建新的线程来执行任务。workQueue
:阻塞队列。当线程池中的线程数超过corePoolSize
,新任务会被放到队列中,等待执行。maximumPoolSize
:线程池的最大线程数量。keepAliveTime
:空闲线程的存活时间。空闲线程就是当池子中的一个线程从阻塞队列中取不到任务了,在等待了keepAliveTime
之后还是没取到任务,就会被回收。unit
:keepAliveTime
的时间单位。threadFactory
:创建线程的工厂。默认的线程工厂会把提交的任务包装成一个新的任务。handler
:拒绝策略。当线程池的workQueue
已满且线程数达到最大线程数时,新提交的任务执行对应的拒绝策略。
JDK也提供了一个快速创建线程池的工具类Executors
,它提供了多种创建线程池的方法,但通常不建议使用Executors
来创建线程池,因为它提供的很多工具方法,要么使用的阻塞队列没有设置边界,要么是没有设置最大线程的上限。任务一多容易发生OOM。实际开发应该根据业务自定义线程池。
4. 源码剖析
4.1. execute
线程池的核心运行机制在于execute
方法,所有的任务调度都是通过execute
方法完成的。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) { // (1)
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) { // (2)
int recheck = ctl.get();
// 重新检查状态,如果是非运行状态,接着执行队列删除操作,然后执行拒绝策略
if (! isRunning(recheck) && remove(command))
reject(command);
// 如果是因为remove(command)删除队列元素失败,再判断池中线程数量
// 如果池中线程数为0则新增一个任务为null的非核心线程
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false)) // (3)
reject(command);
}
透过execute
方法的3个if
判断,可以把它的逻辑梳理为3个部分:
- 第一个
if
:如果线程数量小于核心线程数,则创建一个线程来执行新提交的任务。 - 第二个
if
:如果线程数量大于等于核心线程数,则将任务添加到该阻塞队列中。 else if
:线程池不是运行状态,或者添加到队列失败即队列满了,则创建一个非核心线程执行新提交的任务。如果非核心线程创建失败就执行拒绝策略。
4.2. addWorker
execute
中的核心逻辑要看addWoker
方法,它承担了核心线程和非核心线程的创建。addWorker
方法前半部分代码用一个双重for
循环确保线程池状态正确,后半部分的逻辑是创建一个线程对象Worker
,添加到存储线程对象的HashSet
中,然后使用Worker
线程执行任务的过程。
Worker
是对提交进来的线程的封装,创建的worker
会被添加到一个HashSet
,线程池中的线程都维护在这个名为workers
的HashSet
中并被线程池所管理。
前面说到,Worker
本身也是一个线程对象,它实现了Runnable
接口,在addWorker
中会启动一个新的任务,所以我们要看它的run
方法,而run
方法的核心逻辑是runWorker
方法。
final void runWorker(Worker w) {
// ...
try {
while (task != null || (task = getTask()) != null) {
// ...
try {
try {
task.run(); // 执行普通的run方法
} finally {
task = null; // task置空
}
}
}
} finally {
processWorkerExit(w, completedAbruptly); // 回收空闲线程
}
}
可以看到runWorker
方法中有一个while
循环,循环执行task
的run()
方法,这里的task
就是提交到线程池的任务,它对当成了普通的对象,执行完task.run()
,最后会把task
设置为null
。
再看循环的条件,已知task
会为空,所以我们再看看(task = getTask()) != null
这个条件,如果getTask() == null
则跳出循环执行processWorkerExit
方法,processWorkerExit
方法的作用是回收空闲线程。
4.3. getTask
很多答案都在getTask()
方法中。
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (; ; ) { // (1)
// 校验线程池状态的代码,先省略...
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; // (2)
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c)) // 线程数减1
return null; // 这里是中断外层while循环的时机
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take(); // (3)
if (r != null)
return r; // 取到值了就在外层的while循环中执行任务
timedOut = true; // 否则就标记为获取队列任务超时
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
结合(1)、(3)这两个地方可以看出,getTask()
方法是一个无限循环,不断从阻塞队列中取任务,取到了任务就返回,到外层runWorker
方法中,执行这个任务的run
方法。即线程池通过启动一个Worker子线程来执行提交进来的任务,并且一个Worker线程会执行多个任务!
我们再看看getTask()
何时返回null
,因为返回null
才可以看下一步的processWorkerExit
方法。
getTask()
返回null
主要看timed && timedOut
这个条件。变量值timed
为true
的条件是:允许核心线程超时或者线程数大于核心线程数。timedOut
变量为true
的条件是从workQueue
为空了,取不到任务了,但是这个前提是timed == true
,执行workQueue.poll
的时候,因为workQueue.poll
方法获取任务最多等待keepAliveTime
的时间,超过这个时间获取不到就返回null
,而workQueue.take()
方法获取不到任务会一直等待!
因此,在核心线程不会超时的情况下,如果池中的线程数小于核心线程数,这个getTask()会一直循环下去,这就是在这种情况下线程池不会自动关闭的原因!反之,在核心线程不会超时的情况下,如果池中的线程数超过核心线程数,才会对多余的线程回收。如果allowCoreThreadTimeOut == true
,即核心线程也能超时,当阻塞队列为空,所有Worker
线程都会被回收。
ThreadPoolExecutor
的注释说,当池中没有剩余线程,线程池会自动关闭。
A pool that is no longer referenced in a program AND has no remaining threads will be shutdown automatically
但我也没找到证据,没看到哪里显式调用shutdown()
,但确实会自动关闭。
4.4. processWorkerExit
某个Worker
线程在执行getTask()
获取不到任务后,会执行processWorkerExit
方法回收线程。从HashSet
中remove
该Worker
线程对象。可见线程销毁线程的方式是删除线程引用,让 JVM 自动回收。
private void processWorkerExit(Worker w, boolean completedAbruptly) {
// ...
try {
workers.remove(w);
}
...
}
5. 原理总结
最后我们回到最初的问题,线程池的原理是什么,线程池怎么做到重复利用线程的?
- 线程池通过维护一组叫
Worker
的线程对象来处理任务。 - 已有线程数小于核心线程数时,一个任务开启一个
Worker
线程,超过核心线程数,新任务加到阻塞队列。 - 一个
Worker
线程启动后,除了执行第一次任务之外,还会不断从阻塞队列中消费任务。 - 如果队列里没任务了,
Worker
线程会一直轮询,不会退出,以此达到重复利用线程的目的;只有池中线程数超过核心线程数时才退出轮询,然后回收多余空闲线程,其他核心线程依然轮询。 - 如果核心线程也会超时,那么在队列中无任务时所有线程都会被回收,最后线程池自动关闭。
- 即一个
Worker
线程会处理多个任务,默认情况下核心线程会一直轮询队列,不会退出。
6. 拒绝策略
拒绝策略的目的是保护线程池,避免无节制新增任务。JDK使用RejectedExecutionHandler
接口代表拒绝策略,并提供了4个实现类。线程池的默认拒绝策略是AbortPolicy
,丢弃任务并抛出异常。实际开发中用户可以通过实现这个接口去定制拒绝策略。