上一篇文章,咱们刚讲完了ThreadPoolExecutor线程池的execute的大体总体实现,以及内部的重要属性,尚未看过的,要想看这篇文章,必须先连接上一篇才能继续: juejin.im/post/5d67e5…数据库
这节咱们将接着看线程池是如何终止和中止运行的.直接上源码:编程
tryTerminate方法安全
tryTerminate方法根据线程池状态进行判断是否结束线程池, 什么时候调用: 1.若是加入工做队列失败,尝试调用。 2.processWorkerExit执行work队列中的worker退出。 3.shutdown 启动有序的关闭线程池中的线程,再也不接收新任务,而后关闭工做线程。 代码以下:bash
final void tryTerminate() {
for (;;) {
int c = ctl.get();
/*
* 当前线程池的状态为如下几种状况时,直接返回:
* 1. RUNNING,由于还在运行中,不能中止;
* 2. TIDYING或TERMINATED,由于线程池中已经没有正在运行的线程了;
* 3. SHUTDOWN而且等待队列非空,这时要执行完workQueue中的task;
*/
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
// 若是线程数量不为0,则中断一个空闲的工做线程,并返回
if (workerCountOf(c) != 0) { // Eligible to terminate
interruptIdleWorkers(ONLY_ONE); //该类的解析在上一篇文章,在顶部
return;
}
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 这里尝试设置状态为TIDYING,若是设置成功,则调用terminated方法
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
// terminated方法默认什么都不作,留给子类实现
terminated();
} finally {
// 设置状态为TERMINATED
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}
复制代码
interruptIdleWorkers(ONLY_ONE);的做用是由于在getTask方法中执行workQueue.take()时,若是不执行中断会一直阻塞。并发
在下面介绍的shutdown方法中,会中断全部空闲的工做线程,若是在执行shutdown时工做线程没有空闲,而后又去调用了getTask方法,这时若是workQueue中没有任务了,调用workQueue.take()时就会一直阻塞。因此每次在工做线程结束时调用tryTerminate方法来尝试中断一个空闲工做线程,避免在队列为空时取任务一直阻塞的状况。ide
shutdown方法post
shutdown方法要将线程池切换到SHUTDOWN状态,并调用interruptIdleWorkers方法请求中断全部空闲的worker,最后调用tryTerminate尝试结束线程池。ui
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 安全策略判断
checkShutdownAccess();
// 切换状态为SHUTDOWN
advanceRunState(SHUTDOWN);
// 中断空闲线程
interruptIdleWorkers();
onShutdown();//在调用shutdown时运行状态转换后执行任何进一步的清理。
这里是一个空实现,但由子类 ScheduledThreadPoolExecutor能够用于取消延迟的任务。
} finally {
mainLock.unlock();
}
// 尝试结束线程池
tryTerminate();
}
复制代码
最终要思考一个问题: 在runWorker方法中,执行任务时对Worker对象w进行了lock操做,为何要在执行任务的时候对每一个工做线程都加锁呢?this
在getTask方法中,若是这时线程池的状态是SHUTDOWN而且workQueue为空,那么就应该返回null来结束这个工做线程,而使线程池进入SHUTDOWN状态须要调用shutdown方法;idea
shutdown方法会调用interruptIdleWorkers来中断空闲的线程,interruptIdleWorkers持有mainLock,会遍历workers来逐个判断工做线程是否空闲。但getTask方法中没有mainLock;
在getTask中,若是判断当前线程池状态是RUNNING,而且阻塞队列为空,那么会调用workQueue.take()进行阻塞;
若是在判断当前线程池状态是RUNNING后,这时调用了shutdown方法把状态改成了SHUTDOWN,这时若是不进行中断,那么当前的工做线程在调用了workQueue.take()后会一直阻塞而不会被销毁,由于在SHUTDOWN状态下不容许再有新的任务添加到workQueue中,这样一来线程池永远都关闭不了了;
由上可知,shutdown方法与getTask方法(从队列中获取任务时)存在竞态条件; 解决这一问题就须要用到线程的中断,也就是为何要用interruptIdleWorkers方法。在调用workQueue.take()时,若是发现当前线程在执行以前或者执行期间是中断状态,则会抛出InterruptedException,解除阻塞的状态;
可是要中断工做线程,还要判断工做线程是不是空闲的,若是工做线程正在处理任务,就不该该发生中断;
因此Worker继承自AQS,在工做线程处理任务时会进行lock,interruptIdleWorkers在进行中断时会使用tryLock来判断该工做线程是否正在处理任务,若是tryLock返回true,说明该工做线程当前未执行任务,这时才能够被中断。
下面就来分析一下interruptIdleWorkers方法。
private void interruptIdleWorkers() {
interruptIdleWorkers(false);
}
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
复制代码
interruptIdleWorkers遍历workers中全部的工做线程,若线程没有被中断tryLock成功,就中断该线程。
shutdownNow
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
// 中断全部工做线程,不管是否空闲
interruptWorkers();
// 取出队列中没有被执行的任务
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
复制代码
当即中止全部的执行任务,并将队列中的任务返回
shutdown和shutdownNow区别 shutdown和shutdownNow这两个方法的做用都是关闭线程池,流程大体相同,只有几个步骤不一样,以下 加锁 检查关闭权限 CAS改变线程池状态 设置中断标志(线程池不在接收任务,队列任务会完成)/中断当前执行的线程 调用onShutdown方法(给子类提供的方法)/获取队列中的任务 解锁 尝试将线程池状态变成终止状态TERMINATED 结束/返回队列中的任务
如何合理配置线程池参数? 要想合理的配置线程池,就必须首先分析任务特性,能够从如下几个角度来进行分析:
任务的性质:CPU密集型任务,IO密集型任务和混合型任务。 任务的优先级:高,中和低。 任务的执行时间:长,中和短。 任务的依赖性:是否依赖其余系统资源,如数据库链接。
任务性质不一样的任务能够用不一样规模的线程池分开处理。
CPU密集型任务配置尽量少的线程数量,如配置Ncpu+1个线程的线程池。
IO密集型任务则因为须要等待IO操做,线程并非一直在执行任务,则配置尽量多的线程,如2xNcpu。
混合型的任务,若是能够拆分,则将其拆分红一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,若是这两个任务执行时间相差太大,则不必进行分解。咱们能够经过Runtime.getRuntime().availableProcessors()方法得到当前设备的CPU个数。
优先级不一样的任务可使用优先级队列PriorityBlockingQueue来处理。它可让优先级高的任务先获得执行,须要注意的是若是一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
执行时间不一样的任务能够交给不一样规模的线程池来处理,或者也可使用优先级队列,让执行时间短的任务先执行。
依赖数据库链接池的任务,由于线程提交SQL后须要等待数据库返回结果,若是等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。
而且,阻塞队列最好是使用有界队列,若是采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃。
经过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可使用
getTaskCount:线程池已经执行的和未执行的任务总数; getCompletedTaskCount:线程池已完成的任务数量,该值小于等于taskCount; getLargestPoolSize:线程池曾经建立过的最大线程数量。经过这个数据能够知道线程池是否满过,也就是达到了maximumPoolSize; getPoolSize:线程池当前的线程数量; getActiveCount:当前线程池中正在执行任务的线程数量。 经过这些方法,能够对线程池进行监控,在ThreadPoolExecutor类中提供了几个空方法,如beforeExecute方法,afterExecute方法和terminated方法,能够扩展这些方法在执行前或执行后增长一些新的操做,例如统计线程池的执行任务的时间等,能够继承自ThreadPoolExecutor来进行扩展。
总结 本文比较详细的分析了线程池的工做流程,整体来讲有以下几个内容:
分析了线程的建立,任务的提交,状态的转换以及线程池的关闭; 这里经过execute方法来展开线程池的工做流程,execute方法经过corePoolSize,maximumPoolSize以及阻塞队列的大小来判断决定传入的任务应该被当即执行,仍是应该添加到阻塞队列中,仍是应该拒绝任务。 介绍了线程池关闭时的过程,也分析了shutdown方法与getTask方法存在竞态条件; 在获取任务时,要经过线程池的状态来判断应该结束工做线程仍是阻塞线程等待新的任务,也解释了为何关闭线程池时要中断工做线程以及为何每个worker都须要lock。 在向线程池提交任务时,除了execute方法,还有一个submit方法,submit方法会返回一个Future对象用于获取返回值。
摘自: 《Java并发编程的艺术》 JDK8源码 深刻理解Java线程池 www.ideabuffer.cn/2017/04/04/