概述
在 java 多线程场景下,如果在每次需要使用线程的时候创建它,并且在线程使用结束后回收的话,无论是创建还是回收线程都给系统带来了很大的开销,需要的线程越多,创建回收线程所额外消耗的资源也就越多。在这种场景下,就有了线程池的概念:
提前创建好一部分线程,当需要使用线程的时候,从线程池中拿出来使用,使用完毕后放回线程池,这样就可以省去创建和回收线程的额外消耗。
关于线程池我们可以抽象的理解成这样:假设现在有一家搬家公司(公司为了节省开支没有固定员工),当有客户需要搬家时,就去市场上临时找搬运工,搬运工忙完之后又回归到市场。后来公司老总发现每次去市场找搬运工的开销很大,因此公司招几个固定员工,这里的固定员工就类似线程池中的线程,后面当有客户需求时,就首先让这部分固定员工搬运,如果固定员工不够用的话再去市场上寻找临时搬运工。
一、线程池
下面我们分别从以下8个方面了解线程池:
- 线程池的七大参数:
- 线程池的五种状态:
- 线程池的五大种类:
- 线程池的四大特性:
- 线程池的四种阻塞队列:
- 线程池的四种拒绝策略:
- 线程池工作的原理:
- 线程池的关闭
1、线程池的参数
无论我们用什么方式创建线程池,最终都会走到如下构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize:线程池中核心线程的数量;当提交一个任务到线程池时,线程池会创建一个核心线程执行该任务,即使现在线程池中有其他空闲线程,直到核心线程数达到 corePoolSize 才不在创建,此时线程池会把该任务放到阻塞队列中。如果调用线程池的 preStartAllCoreThreads 方法,那么核心线程会在线程池初始化的时候创建。
maximumPoolSize:线程池中最大线程的数量,如果阻塞队列已满,并且当前线程数低于 maximumPoolSize 的话,就会创建新的线程执行任务。
keepAliveTime:线程最大空闲保持时长,指的是工作线程空闲之后持续的时间,达到该时长的工作线程将会被回收。默认情况下,这个参数只有线程数大于 corePoolSize 的时候才会起作用,一直回收到当前线程数等于 corePoolSize 。如果调用 allowCoreThreadTimeOut 方法的话,基于当前线程数低于 corePoolSize 也会回收线程,直到线程数等于0。
unit:参数 keepAliveTime 的时间单位。
workQueue:阻塞队列,当线程池中线程数量等于 corePoolSize 时,新来的任务会被放到阻塞队列中,关于阻塞队列的详细介绍在下面模块五详细说明。
threadFactory:用于创建线程的线程工厂。
handler:当阻塞队列已满,并且线程池中线程数等于 maximumPoolSize 的话,新来的任务必须通过一种策略进行处理,这里的 handler 就是线程池饱和状态下的策略。关于拒绝策略的详细介绍在下面模块六详细说明
2、线程池的状态
关于线程池的状态,代码中通过以下五个常量来表示:
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;
每个状态的工作方式如下所示:
- RUNNING:可以接受任务,并且可以处理阻塞队列中的任务。
- SHUTDOWN:不能接受任务,可以处理阻塞队列中的任务。
- STOP:不能接受任务,不能处理阻塞队列中的任务,同时中断正在处理的任务。
- Tidying:属于过渡阶段,在这个阶段表示所有的任务已经执行结束了,当前线程池中是不存在有效的线程的,并且将要调用terminated 方法。
- TERMINATED:终止状态
线程池中这五种状态的转换关系在代码注释中已经有详细的介绍:
* RUNNING -> SHUTDOWN
* On invocation of shutdown(), perhaps implicitly in finalize()
* (RUNNING or SHUTDOWN) -> STOP
* On invocation of shutdownNow()
* SHUTDOWN -> TIDYING
* When both queue and pool are empty
* STOP -> TIDYING
* When pool is empty
* TIDYING -> TERMINATED
* When the terminated() hook method has completed
具体转换策略如下图所示:
3、线程池的种类
3-1、SingleThreadPool
public static ExecutorService newSingleThreadExecutor() {
return new Executors.FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- 核心线程数和最大线程数都是1
- 阻塞队列是无界队列
创建只有一个核心线程的线程池,主要作用是保证任务的顺序执行。即使在执行某个任务过程中因为出错线程停止,线程池也会创建新的线程保证任务的顺序执行,并保证任何时刻只有一个线程工作。
3-2、newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
- 核心线程数和最大线程数相同
- 阻塞队列是无界队列
创建一个最大线程数固定且都是核心线程的线程池,主要作用是可以很好的控制并发。
3-3、newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- 没有核心线程,最大线程数为 MAX_VALUE
创建一个没有核心线程的线程池,理论上可以有 MAX_VALUE 个工作线程,并且默认情况下,空闲时间达到 60S 的线程将会被回收。主要作用是应对任务不规律的场景,假如长时间都没有任务,那么所有工作内存都会被回收,节省内存,当任务比较多的时候也可以创建大量的工作线程进行处理
3-4、ScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new ScheduledThreadPoolExecutor.DelayedWorkQueue());
}
- 有固定核心线程数,最大线程数为 MAX_VALUE
- 任务队列会根据任务延时时间的优先级进行执行
创建一个固定核心线程数的线程池,场景有点类似 概述 中搬家公司的例子。它的阻塞队列可配置定时或者延时执行任务。主要作用是可以做定时器,并且在没有任务时,保证线程池中只有核心线程,此时如果有任务需要执行可以直接使用核心线程执行,当需要执行任务的填满阻塞队列时,再创建工作线程,减少资源耗费。
3-5、SingleThreadScheduledPool
public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
return new Executors.DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1, threadFactory));
}
- 有且只有一个核心线程
- 可定时或延时执行任务
创建只包含一个核心线程的线程池,和 SingleThreadPool 的区别是,它可以做定时或延时任务,主要作用是做定时器
4、线程池的特性
无论哪种线程池都满足以下特性,这些特性也是线程池执行的规则:
- 当线程池中的线程数小于 corePoolSize 时,无论是否有空闲线程,创建新线程执行任务
- 当线程池中线程数大于等于 corePoolSize 时,无论是否有空闲线程,任务首先进入阻塞队列
- 当阻塞队列已满,并且线程池中线程小于 maximumPoolSize 时,创建新的工作线程执行该任务
- 当阻塞队列已满,并且线程池中线程等于 maximumPoolSize 时,对于新加入的任务,按照拒绝策略执行。
5、线程池的阻塞队列
在不同的线程池中需要使用不同的阻塞队列,就比如在 ScheduledThreadPool 中,如果使用没有界限的阻塞队列,那么就永远无法创建工作线程,也就失去了它的意义,下面我们介绍几种线程池中常用的阻塞队列:
5-1、SynchronousQueue
- 阻塞队列的默认选项,不存储元素的阻塞队列
- 将任务直接提交给线程,如果当前没有可用空闲线程,就创建新的线程
线程池 CachedThreadPool 默认使用,理论上它可以创建 MAX_VALUE 个线程,因此阻塞队列对于任务可以直接提交。
5-2、ArrayBlockingQueue
- 基于数组的有界阻塞队列
默认的线程池构造中没有使用该阻塞队列,如果不考虑定时或者延时执行任务,ScheduledThreadPool 可以使用该阻塞队列。因为该队列基于数组实现,对于频繁的删除修改节点,不建议使用该阻塞队列。
5-3、LinkedBlockingQueue
- 基于链表实现的无界阻塞队列
线程池 newFixedThreadPool 、 SingleThreadPool 默认使用该阻塞队列。因为链表的原因,可以快速按顺序执行,并且因为无界,所以理论上所有任务都可以被处理,缺点是当任务处理的速度小于任务加入队列的速度时,可能导致阻塞队列越来越长,最终导致后序所有请求都无法被及时处理。
5-4、DelayedWorkQueue
- 基于优先级的有界阻塞队列
线程池ScheduledThreadPool、SingleThreadScheduledPool 默认使用该阻塞队列。主要用来做定时以及延时执行,其中该队列通过堆实现
6、线程池的拒绝策略
线程池的拒绝策略是自定义的,其中线程池系统为我们提供了以下4种常见的拒绝策略:
AbortPolicy:直接抛出异常
DiscardPolicy:直接丢弃该任务
DiscardOldestPolicy:丢弃阻塞队列中最近(队列头部)的任务,并尝试执行该任务,如果执行失败,继续丢弃。
CallerRunsPolicy:使用调用线程池的任务来执行该任务
7、线程池的工作原理
关于线程池的工作原理,主要从线程池执行一次任务的全流程说起:
线程池有 submit() 以及 execute() 两种方法来执行任务,其中 submit() 方法可以通过 Future 对象获取线程执行完的返回值,而 execute() 方法不行。其中 submit() 方法中会调用 execute() 方法,因此我们主要看 execute() 方法的执行策略。
根据上面的介绍,我们也可以知道:execute() 方法执行过程中如果出现异常,异常会在线程池内部被消耗。而 submit() 方法则可以抛出异常,通过返回值将异常抛出到外面。
根据上面线程池特性的介绍,在线程池执行 execute() 方法时会有以下三种情况:
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
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);
情况一 :如果当前线程数量小于 corePoolSize :直接调用 addWorker() 方法创建一个新的 Worker对象 来执行我们当前的任务。
情况二:如果当前线程数量大于等于 corePoolSize 并且线程状态处于 RUNNING,那么我们首先将该任务放入阻塞队列中,并且再次检查线程池的状态,如果线程池不在 RUNNING 状态的话,就会回滚刚才添加到队列的操作,并且放弃该任务。如果处于 RUNNING 状态,那么检查当前工作线程数量是否等于0,如果等于0则创建一个线程执行该任务
情况三:如果我们无法将该任务排队,就尝试添加一个新线程处理该任务,如果失败,就抛弃该任务。
看到这里,可能会疑惑,为什么没有出现 maximumPoolSize 变量,这里是因为 maximumPoolSize 变量的判断其实都写在了addWorker() 方法中。
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
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
}
}
这里我们可以看到,在添加 Worker对象 前,首先判断当前线程池的状态是否允许添加线程,然后判断当前线程的工作线程数是否已经超过 maximumPoolSize 变量,如果没有超过时,才尝试修改线程数量标识,如果修改成功就跳出循环。
看到这里大家也应该可以理解了,一个 Worker对象 实际上就是一个工作线程,下面我们来看 Worker对象 的实现。
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
Worker(Runnable firstTask) {
setState(-1);
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
}
上述是我截取之后的部分 Worker对象 的源码,其中 firstTask 属性就是我们在 addWorker() 方法中传过去的任务,在获取到任务之后,通过线程池构造方法中的 threadFactory 创建线程,其中该线程所执行的就是我们传过去的任务。
这里可能有点饶,我大概给大家介绍一下,java 创建一个线程常见有三种方法,我这里主要介绍两种
- 继承 Thread 类,重写 run() 方法,该方法就是线程的执行体
- 实现 Runnable 接口,重写 run() 方法,将该对象作为参数传递给 Thread对象
在我们调用 addWorker() 方法后,会调用 Worker 对象内部封装 Thread 的 start() 方法。
Worker w = null;
w = new Worker(firstTask);
final Thread t = w.thread;
t.start();
其中这里我省去了大量的判断逻辑,感兴趣的读者可以试着阅读源码。这里内部 thread 参数就是通过上述方法二,把 Worker对象作为参数创建的,执行它的 start() 方法,也就是调用 Worker对象 的 run() 方法。
public void run() {
runWorker(this);
}
final void runWorker(ThreadPoolExecutor.Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
boolean completedAbruptly = true;
try {
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 (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
这里我们看到在 Worker对象 的 run() 方法会调用 runWorker() 方法,在该方法中首先判断任务是否为空,并且在执行之前还会判断一次当前线程池的状态,如果是 STOP 状态,会试着停止这些线程。如何检验没有问题,最终会通过 task.run() 这一步执行提交的任务。看到这里我们大概就了解了 execute() 方法的执行过程。
关于上面的 runWorker() 方法其实还有一个细节没有注意:当 firstTask 为空时,会通过 getTask() 方法从阻塞队列中获取任务。这也是为什么线程池可以复用线程的原因,通过修改 Runnable属性 来做到同一个线程执行不同的任务。以下这几种情况 getTask() 方法会返回空:
- 工作线程数超过 maximumPoolSize
- 线程池处于 STOP 状态
- 线程池处于 SHUTDOWN 状态,并且队列为空
- 工作线程已经超过最大等待时长
工作线程会一直尝试从缓存队列中获取任务,直到无法取出任务时,就会跳出循环执行 processWorkerExit() 方法。执行该方法会给当前 Worker对象 一个标识,通过该标识告诉该对象需要回收,在回收过程中会检查线程池的状态,如果线程池处于停止状态并且阻塞队列和工作线程都为空,那么就将线程池状态过渡到 Tidying ,之后调用 terminated() ,将线程池彻底终止。
最后我们再试着看看 submit() 方法的源码:
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
实际上, submit() 方法也是通过调用 execute() 方法来执行任务,不同的点是它传的任务是 RunnableFuture 接口类型 ,其中RunnableFuture 接口继承 Runnable 接口,它内部通过继承 Future接口 来获取任务返回值。
8、线程池的关闭
执行 shutdown() 方法,线程池不再处理新添加的任务,只执行阻塞队列中的任务,在执行阻塞队列任务的同时,回收空闲的 Worker对象,在等待阻塞队列中所有任务执行完毕后,并且所有 Worker对象 被回收后,将线程池状态过渡到 Tidying ,之后调用 terminated() ,将线程池彻底终止。
或者说直接执行 shutdownnow() 方法,线程池不再处理新添加的任务,并且尝试停止当前正在运行的线程,并且将阻塞队列中所有未执行的线程取出来,作为返回值返回,也就是说 shutdownnow() 方法会返回还没有执行的阻塞队列中的任务。当所有 Worker对象 被回收后,将线程池状态过渡到 Tidying ,之后调用 terminated() ,将线程池彻底终止。
awaitTermination(long timeout, TimeUnit unit) 方法阻塞的判断线程池是否处于 TERMINATED 状态,当处于 TERMINATED 状态时返回 TRUE,其中该方法的参数表示最长等待时间,如果达到该时间还没有处于 TERMINATED 状态则返回 FALSE