JDK源码分析之concurrent包(二) -- 线程池ThreadPoolExecutor
上一篇我们简单描述了Executor框架的结构,本篇正式开始并发包中部分源码的解读。
我们知道,目前主流的商用虚拟机在线程的实现上可能会有所差别。但不管如何实现,在开启和关闭线程时一定会耗费很多CPU资源,甚至在线程的挂起和恢复JDK1.6都做了自旋锁的优化。所以,使用线程池来管理和执行多线程任务会大大提高程序执行效率。关于使用线程池的优点这里不做过多说明,我们直接进入Java5并发包中ThreadPoolExecutor的实现的源码。
在解读源码前,我们先来看看创建线程池的一般做法和线程池的几种类别:
Executors.newFixedThreadPool(int nThreads); // 创建一个固定线程数的线程池
Executors.newScheduledThreadPool(int nThreads); // 创建一个可对线程进行时间调度的线程池
Executors.newCachedThreadPool(); // 创建一个可缓冲的无线程数量界限(Integer.MAX_VALUE)的线程池
Executors.newSingleThreadExecutor(); // 创建一个可复用的单一线程的线程池
我们重点来看1、3、4条,在Executors中如何实现的
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
可以看到,差别只是ThreadPoolExecutor的构造方法的参数不同,下面来看看ThreadPoolExecutor的构造方法的参数(按顺序):
- corePoolSize - 池中所保存的线程数,包括空闲线程。
- maximumPoolSize - 池中允许的最大线程数。
- keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
- unit - keepAliveTime 参数的时间单位。
- workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务。
- threadFactory - 执行程序创建新线程时使用的工厂。
- handler - 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。
从参数说明中看出,1、3、4中的线程池主要是“核心线程数”和“最大线程数”的差别,而keepAliveTime和workQueue的差别是由“核心线程数”和“最大线程数”是否相等来决定的。那么“核心线程数”和“最大线程数”分别代表什么?带着这个疑问进入execute方法,源码如下:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
else if (!addIfUnderMaximumPoolSize(command))
reject(command); // is shutdown or saturated
}
}
第4行的代码表达一件事:当线程池中当前线程数小于核心线程数时,执行addIfUnderCorePoolSize(command)方法,并且执行成功后不再执行后面的逻辑。那我们就来看看这个addIfUnderCorePoolSize(command)方法做了什么:
/**
* Creates and starts a new thread running firstTask as its first
* task, only if fewer than corePoolSize threads are running
* and the pool is not shut down.
* @param firstTask the task the new thread should run first (or
* null if none)
* @return true if successful
*/
private boolean addIfUnderCorePoolSize(Runnable firstTask) {
Thread t = null;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (poolSize < corePoolSize && runState == RUNNING)
t = addThread(firstTask);
} finally {
mainLock.unlock();
}
if (t == null)
return false;
t.start();
return true;
}
方法注释的主要意思是:当运行线程少于核心线程时,就创建并运行一个新的线程。代码的第15行创建了一个新的线程,第21行运行了这个线程。接下来看看如何创建的这个线程:
private Thread addThread(Runnable firstTask) {
Worker w = new Worker(firstTask);
Thread t = threadFactory.newThread(w);
if (t != null) {
w.thread = t;
workers.add(w);
int nt = ++poolSize;
if (nt > largestPoolSize)
largestPoolSize = nt;
}
return t;
}
第二行可以看到,线程池中真正执行的线程是由名为Worker的内部类来执行的,关于Worker的主要结构和方法如下:
(注:addThread方法的注释中强调了要在持有mainLock的锁时才能调用,mainLock锁在线程池的安全并发的实现中担任着非常重要的角色,并且对于firstTask,有一点不同的逻辑在,由于篇幅有限,本文这里不做重点解读了)
private final class Worker implements Runnable { // others codes /**
* Main run loop
*/
public void run() {
try {
Runnable task = firstTask;
firstTask = null;
while (task != null || (task = getTask()) != null) {
runTask(task);
task = null;
}
} finally {
workerDone(this);
}
}
}
可以看到,Worker实现了Runnable接口,线程池中执行的线程其实是Worker的run()方法。而第13行的runTask(task)方法的实现是直接调用了提交到线程池中的Runnable任务的run方法(具体代码请自行查看源码,这里不再列出,其中还包含一些针对shutdown和shutdownNow的逻辑),还有比较重要的是第12行的getTask()方法,最后来看getTask()的源码:
Runnable getTask() {
for (;;) {
try {
int state = runState;
if (state > SHUTDOWN)
return null;
Runnable r;
if (state == SHUTDOWN) // Help drain queue
r = workQueue.poll();
else if (poolSize > corePoolSize || allowCoreThreadTimeOut)
r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
else
r = workQueue.take();
if (r != null)
return r;
if (workerCanExit()) {
if (runState >= SHUTDOWN) // Wake up others
interruptIdleWorkers();
return null;
}
// Else retry
} catch (InterruptedException ie) {
// On interruption, re-check runState
}
}
}
以上代码第13行将线程池保持线程不关闭的实现已经展示出来了:由一个死循环不断的从队列中取出提交到线程池中的Runnable任务,然后直接调用其run()方法即可。
基于这个原理,我们就会很容易的看懂其它的一些特性。
让我们先回头看看关于“核心线程”的源码,回到最开始的execute()的源码:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
else if (!addIfUnderMaximumPoolSize(command))
reject(command); // is shutdown or saturated
}
}
前面我们说根据第4行,当线程池中当前线程数小于核心线程数时,执行addIfUnderCorePoolSize(command)方法并不再执行后面的代码。而当当前线程数大于等于核心线程数时,就会直接执行第5行的workQueue.offer(command),将新任务添加到名为workQueue队列中,也就是死循环中不断取Runnable任务的队列。这里这个workQueue是由构造方法传进来的workQueue队列。通过Executors创建线程池的1、3、4条种类可以看出,核心线程=最大线程的线程池,使用最大容量(Integer.MAX_VALUE)的LinkedBlockingQueue队列,就是说,线程池无法扩展,超出的Runnable任务全部进入阻塞队列中,等待Worker执行完。而核心线程<最大线程的线程池,使用无容量的SynchronousQueue队列,就是说,线程池可以无限扩展,扩展的线程全部新建Worker并执行。但根据getTask()方法的第10行和第11行,超出核心线程数的Worker,空闲时只会存活keepAliveTime时间(构造方法的参数)。
OK,到这里,通过源码已经解释了ThreadPoolExecutor线程池主要的特性的实现原理。
上面罗里吧嗦的一大堆主要说明了JDK源码中实现的ThreadPoolExecutor线程池的以下几个主要特性(来自JDK API的描述):
核心线程数与最大线程数的意义:
ThreadPoolExecutor将根据corePoolSize和maximumPoolSize设置的边界自动调整池大小。当新任务在方法 execute(java.lang.Runnable)中提交时,如果运行的线程少于corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的。如果运行的线程多于corePoolSize而少于maximumPoolSize,则仅当队列满时才创建新线程。如果设置的corePoolSize和maximumPoolSize相同,则创建了固定大小的线程池。如果将 maximumPoolSize设置为基本的无界值(如Integer.MAX_VALUE),则允许池适应任意数量的并发任务。
保持活动时间:
如果池中当前有多于 corePoolSize 的线程,则这些多出的线程在空闲时间超过 keepAliveTime 时将会终止
排队:
所有
BlockingQueue
都可用于传输和保持提交的任务。可以使用此队列与池大小进行交互:
- 如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。
- 如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。
- 如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。
其它的特性,如终止线程池的几种方式及被拒绝的任务由构造方法传入的handler处理等本文并未给出源码解读,感兴趣的读者可自行查看JDK源码。
另外,关于ThreadPoolExecutor的子类ScheduledThreadPoolExecutor,本文不打算详细介绍了。其核心原理是一样的,只是多了“Schedule”的功能。而这个任务调度的功能是通过构造时传入的DelayQueue来实现的,大家如果感兴趣可以看下DelayQueue的介绍:“Delayed元素的一个无界阻塞队列,只有在延迟期满时才能从中提取元素”。“延迟期满”的原理是通过lock包中ReadWriteLock锁获取的Condition的awaitNanos(long nanosTimeout)方法来实现的。
总结
本文通过部分关键处源码的解读,介绍了ThreadPoolExecutor线程池的实现原理。我个人简单总结为两点:
- 线程池中真正执行的线程是由名为Worker的内部类来执行的
- 执行的方式是由一个死循环不断的从队列中取出提交到线程池中的Runnable任务,然后直接调用其run()方法
这两点只是做概括,真正展开来描述,还是有很多细节的。
JDK源码分析之concurrent包(二) -- 线程池ThreadPoolExecutor的更多相关文章
- JDK源码分析之concurrent包(一) -- Executor架构
Java5新出的concurrent包中的API,是一些并发编程中实用的的工具类.在高并发场景下的使用非常广泛.笔者在这做了一个针对concurrent包中部分常用类的源码分析系列.本系列针对的读者是 ...
- JDK源码分析之concurrent包(三) -- Future方式的实现
上一篇我们基于JDK的源码对线程池ThreadPoolExecutor的实现做了分析,本篇来对Executor框架中另一种典型用法Future方式做源码解读.我们知道Future方式实现了带有返回值的 ...
- JDK源码分析之concurrent包(四) -- CyclicBarrier与CountDownLatch
上一篇我们主要通过ExecutorCompletionService与FutureTask类的源码,对Future模型体系的原理做了了解,本篇开始解读concurrent包中的工具类的源码.首先来看两 ...
- Solr4.8.0源码分析(3)之index的线程池管理
Solr4.8.0源码分析(3)之index的线程池管理 Solr建索引时候是有最大的线程数限制的,它由solrconfig.xml的<maxIndexingThreads>8</m ...
- JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue
JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue 目的:本文通过分析JDK源码来对比ArrayBlockingQueue 和LinkedBlocki ...
- 【JDK】JDK源码分析-HashMap(1)
概述 HashMap 是 Java 开发中最常用的容器类之一,也是面试的常客.它其实就是前文「数据结构与算法笔记(二)」中「散列表」的实现,处理散列冲突用的是“链表法”,并且在 JDK 1.8 做了优 ...
- 【JDK】JDK源码分析-ArrayList
概述 ArrayList 是 List 接口的一个实现类,也是 Java 中最常用的容器实现类之一,可以把它理解为「可变数组」. 我们知道,Java 中的数组初始化时需要指定长度,而且指定后不能改变. ...
- 【JDK】JDK源码分析-AbstractQueuedSynchronizer(3)
概述 前文「JDK源码分析-AbstractQueuedSynchronizer(2)」分析了 AQS 在独占模式下获取资源的流程,本文分析共享模式下的相关操作. 其实二者的操作大部分是类似的,理解了 ...
- 手机自动化测试:appium源码分析之bootstrap十二
手机自动化测试:appium源码分析之bootstrap十二 poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.如果对课程感兴趣 ...
随机推荐
- Firefly 3288又一次制作android和lubuntu双系统固件
又一次制作android和lubuntu双系统固件 因为本人改动了lubuntu的驱动和设备树信息,为了方便烧写系统,所以又一次制作了双系统的固件. Firefly wiki教程里有android固件 ...
- 要练习的lambda
取list的id 放入新List<Integer> List<Integer> list1 = list.stream().map(albumGroup1 -> albu ...
- 王立平-- Swift
Swift是供iOS和OS X应用编程的新编程语言,基于C和Objective-C.而却没有C的一些兼容约束.Swift採用了安全的编程模式和加入现代的功能来是的编程更加简单.灵活和有趣. 界面则基于 ...
- css 的通用样式 设置 和倒计时功能 移动轮播图的手势滑动的功能
body{ line-height:1.4; color:#333; font-family:arial; font-size: 12px; background:white; } input,tex ...
- DOCTYPE 与浏览器模式分析
DOCTYPE 的诞生 DOCTYPE,或者称为 Document Type Declaration(文档类型声明,缩写 DTD).通常情况下,DOCTYPE 位于一个 HTML 文档的最前面的位置, ...
- JSP、servlet、SQL三者之间的数据传递
JSP.servlet.SQL三者之间的数据传递 博客分类: web开发 JSPservletSQL数据库连接池web开发 前言: 最近一直在做WEB开发,现总结一下这一段时间的体会和感触. 切记, ...
- How to add total page to Odoo Report(7.0 version)
add this codes to your rml file: <drawRightString x="19.5cm" y="0.3cm">Pag ...
- 用C/C++扩展你的PHP 为你的php增加功能
英文版下载: PHP 5 Power Programming http://www.jb51.net/books/61020.html PHP取得成功的一个主要原因之一是她拥有大量的可用扩展.web开 ...
- Flow construction SGU - 176 有源汇有上下界最小流 二分法和回流法
/** 题目:Flow construction SGU - 176 链接:https://vjudge.net/problem/SGU-176 题意: 有源汇有上下界的最小流. 给定n个点,m个管道 ...
- 自动化软件部署的shell脚本
在实际项目中,我们经常用到需要自动化部署软件的情况,这种情况下,我们就不能人为地在命令行下敲击命令了,一切都需要通过shell脚本自动化完成.所谓自动化完成,其实也就是通过写shell程序,模拟人为敲 ...