Executor框架及几种线程池原理剖析
为什么要用线程池
在程序中创建和销毁对象是很费时间的,在Java中更是如此。虚拟机需要跟踪每个线程中每个对象的生命周期,以便在适当的时候进行垃圾回收。所以,提高程序总体效率的一个手段就是尽可能减少创建和销毁对象的次数。特别是一些很耗资源的对象创建和销毁,在这种背景下就产生了"池化技术"。线程池就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候不需要创建而是从池中获取线程,使用完毕也不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
线程池有以下优点。
- 降低线程创建和销毁线程的开销。
- 因为当任务来临时,不需要等待线程创建而直接执行任务。
- 合理设置线程数量,避免因线程数超过系统硬件资源而出现系统雪崩的情况。Hystrix框架就很好的解决了这个问题。
Executor框架
Executor框架是JDK1.5中引入的,如类之间的关系如图所示。
Executor及ExecutorService接口
其中Executor接口它对给定命令command在将来某个时刻执行。Executer接口类只有一个接口方法。
void execute(Runnable command);
从方法可知,该接口有一些局限性。入口方法是Runnable,它的run()方法是没有返回值的,这将导致它没法把运行结果返回给调用者。它只能执行单个可运行任务,没法正常关闭。但是这些问题都在接口ExecutorService中扩充了这些方法。
接口ExecutorService中大部分方法被抽象类AbstractExecutorService实现。而最重要的实现类是ThreadPoolExecutor,它是所有线程池的核心类,而ThreadPoolExecutor中通过可重入ReetrantLock控制工作线程集的并发访问,而ReetrantLock的核心也是AQS。下面重点讲解下该类中的线程池原理。
ThreadPoolExecutor类
ThreadPoolExecutor域
ThreadPoolExecutor的属性比较多。下表展示了大部分属性。
AtomicInteger ctl | Ctl是线程池数量及控制状态位,是一个原子型整数包装类型。 |
---|---|
workerCount | 有效线程数量 |
runState | 线程池运行状态,小于0时表示线程池执行器没有关闭 |
COUNT_BITS | 默认29,表示支持的最大线程为2^29次方个。 |
CAPACITY | 0x1FFF FFFF |
RUNNING | 0xE000 0000 |
SHUTDOWN | 0x0000 0000 |
STOP | 0x2000 0000 |
TIDYING | 0x4000 0000 |
TERMINATED | 0x6000 0000 |
BlockingQueue workQueue | 阻塞队列,4种线程池中用了3种阻塞队列。分别是newFixedTheadPool和newSingleThreadExecutor中的LinkedBlockingQueue;newCachedThreadPool中的SynchronousQueue;newScheduledThreadPool中的DelayedWorkQueue。 |
ReentrantLock mainLock | ReentrantLock,使用重入锁控制工作线程集的并发访问。 |
HashSet workers | 包含线程池中所有工作线程的集合。仅在持有mainLock时访问 |
Condition termination | mainLock.newCondition(),等待条件来支持等待中断。 |
largestPoolSize | 跟踪当前线程池大小 |
long completedTaskCount | 计数完成的任务,仅仅在中断工作线程时更新。 |
volatile ThreadFactory threadFactory | 线程工厂 |
注:阻塞队列的概念是,一个指定长度的队列,如果队列满了,添加新元素的操作会被阻塞等待,直到有空位为止。
为了能把workerCount和runState包装到一个int中,设计者限定了线程数量为(2^29)-1(大概是500 million)而不是int最大值20亿(2^31)-1,而高3位用来保存执行器的状态,如果将来这是个问题,可以采用AtomicLong进行替换,这将涉及到移位和掩模处理。但是直到现在,使用int无疑是简单有效的。
关于INT的高3位及低29位的意义。
// runState存储在高阶位中
private static final int COUNT_BITS = Integer.SIZE - 3;//32-3=29
private static final int CAPACITY = (1 << COUNT_BITS) - 1;//0x1FFF FFFF
private static final int RUNNING = -1 << COUNT_BITS;//0xE000 0000,
private static final int SHUTDOWN = 0 << COUNT_BITS;//0x0000 0000,
private static final int STOP = 1 << COUNT_BITS;//0x2000 0000,
private static final int TIDYING = 2 << COUNT_BITS;//0x4000 0000,
private static final int TERMINATED = 3 << COUNT_BITS;//0x6000 0000,
// 打包和解包ctl参数
private static int runStateOf(int c) { return c & ~CAPACITY; }//c & 0xE000 0000
private static int workerCountOf(int c) { return c & CAPACITY; }// c & 0x1FFF FFFF
private static int ctlOf(int rs, int wc) { return rs | wc; }
下面对RUNNING状态为 -1 << COUNT_BITS进行分析。
-1的二进制原码为:1000 0000 0000 0001,其中高位1表示符号位。1表示负数,0表示正数。
但是在计算机中负数是以其正值的补码形式表达。即,现在要计算-1的补码。
补码:反码加1为补码。那什么是反码呢?
反码:将一个数的二进制按位取反。
所以,-1的正值为1,1的反码为1111 1111 1111 1111 1111 1111 1111 1110,而-1的补码为:1111 1111 1111 1111 1111 1111 1111 1110 + 1 = 1111 1111 1111 1111 1111 1111 1111 1111 = 0xFFFFFFFF。即RUNNING = (0xFFFFFFFF << 29位),RUNNING = 1110 0000 0000 0000 0000 0000 0000 0000 B = 0xE000 0000。根据此方法依次计算线程各个状态的值,如上图代码中所示。可以发现,运行状态<0,关闭状态(SHOWDOWN)=0,其他状态>0。线程状态大小值为RUNNING<SHUTDOWN(0)<STOP<TIDYING<TERMINATED。
workerCount是工作线程的数量,而工作线程指的是提交开始且未停止的线程。这值可能与实际的活动线程(live threads)在某个时刻可能不同。
runState提供了线程池主要的生命周期控制,有以下值:
- RUNNING: 接收新任务或提供队列任务
- SHUTDOWN:不能接收新任务,但是能处理队列任务
- STOP:不能接收新任务,不能处理队列任务,可中断正在处理的任务。
- TIDYING:所有的任务都已经终止。workerCount为0,线程通过运行terminated()钩子方法过度到TIDYING状态。这也是强制中断线程的一种方式,当前线程会通过tryTerminate方法进入死循环阻塞直到获取锁,也就是说不一定能及时中断。
- TERMINATED:terminated()方法执行完成。
这些值之间的数字顺序很重要,以便进行有序比较。runState随时间单调地增加,但不必命中每个状态。转换关系如下:
- RUNNING -> SHUTDOWN调用shutdown()转换。可能隐式地在finalize()方法中。
- RUNNING or SHUTDOWN -> STOP调用shutdownNow()
- SHUTDOWN -> TIDYING当队列和池都为空时
- STOP -> TIDYING当池为空
- TIDYING -> TERMINATED当terminated()钩子方法调用完成。
ThreadPoolExecutor构造方法
ThreadPoolExecutor构造方法有多个,其中参数最多的构造方法有7个参数。各个参数如下所示。
-
corePoolSize:核心线程数,池中长期保存的活跃线程数。在默认情况下,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到coolPoolSize大小后,把新到的任务放入到缓存队列;当任务都执行完后且线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程来处理,再次让corePoolSize达到设定大小。如果设置了allowCoreThreadTimeOut参数,并在闲置超过keepAliveTime后核心线程数将被销毁。
-
maximumPoolSize:线程池中允许的最大线程数,线程池中的线程总数不得超过该值,大于coolPoolSize且小于maximumPoolSize的线程将在闲置超过keepAliveTime后被销毁。
-
keepAliveTime:当线程数大于核心线程数时,多余线程(最大线程数-核心线程数)等待新任务的保留时间。如果设置了 allowCoreThreadTimeOut(boolean)方法,该参数也会起作用,直到线程池中的线程数为0。如果设置为0,表示线程执行完后就被销毁。默认为1分钟。
-
unit:keepAliveTime的时间单位。
-
workQueue:用阻塞队列来实现的工作队列,该队列用在执行任务之前用于保存任务。此队列只能接收execute方法提交的Runnable任务。线程从workQueue中取任务,若无任务则阻塞等待。
队列类型有三种,分别是默认队列SynchronousQueue ,有界队列ArrayBlockingQueue ,无界队列LinkedBlockingQueue。
SynchronousQueue:它将任务直接提交给线程而不存储它们。所以,使用此队列时,maximumPoolSize参数将直接决定最大等待运行的任务数量。 实际上它不是一个真正的队列,因为它不会为队列中元素维护存储空间。与其他队列不同的是,它维护一组线程,这些线程在等待着把元素加入或移出队列。 该队列在某次添加元素后必须等待其他线程取走后才能继续添加 。 使用SynchronousQueue的目的就是保证“对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务”。
ArrayBlockingQueue:此队列不常用。 指定一个队列长度,当这个队列已满时,才会去使用非corePool的线程执行新的任务。
LinkedBlockingQueue: 无界队列将导致maximumPoolSize的值无效 。当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列。无界队列在某些不适用的场合下将会导致系统资源耗尽。
- threadFactory:执行器创建新线程的线程工厂。 默认使用Executors.defaultThreadFactory() 。
- handler。因为线程边界或者队列容量到达而阻止执行时要使用的处理程序。有4种拒绝策略。
public ThreadPoolExecutor( int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
线程池工作过程
线程池总体工作过程如下图所示。
ThreadPoolExecutor执行execute方法的JDK1.8源码入口如下,处理包括三步。
- 如果运行线程数少于核心线程数,新来任务将创建新的线程执行。通过调用addWorker方法原子检查runState和workerCount,所以通过返回false来防止在不应该添加线程时出现错误警告。
- 如果任务成功加入到队列,我们仍需要再次检查是否需要添加线程(因为上次检查以来可能会有线程死亡)或者线程池是否在进入此方法后关闭。因此,我们有必要检查runState,如果停止则回滚入队,如果没有则启动新线程。
- 如果无法将任务排队,则尝试添加一个新线程。如果失败,我们知道执行器被关闭或者已经饱和。所以拒绝任务。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();//最先开始为0xE000 0000
//1.工作线程最先为0,0xE000 0000 & 0x1FFF FFFF = 0,当工作线程小于核心线程时,新建一个线程执行任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//2.如果核心线程池满了,但任务队列未满,添加到队列中
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//3.任务成功添加到队列后,再次检查是否需要添加新的线程,因为已存在的线程可能被销毁
if (! isRunning(recheck) && remove(command))
reject(command);//如果线程池处于非运行状态,并且把当前的任务从任务队列中成功移除,则拒绝该任务
else if (workerCountOf(recheck) == 0)//如果之前的所有线程已被销毁完,则新建一个线程
addWorker(null, false);
}
else if (!addWorker(command, false))//第2步不满足,则表示核心线程池已满,队列也满了,尝试新建一个线程
reject(command);//创建失败,说明线程池被关闭或者连最大线程池也满了,则拒绝该任务
}
下面分析添加任务addWorker方法。
- firstTask:新线程首先运行的任务(如果没有则为空)。当少于核心线程(corePoolSize )(在这种情况下,我们总是开启一个线程)或当队列已满(在这种情况下,我们必须绕过队列)时,使用初始的第一个任务(通过execute方法)来绕过队列创建工作线程。最初空闲线程通常通过prestartCoreThread创建或者换掉其他垂死的工作线程。
- core: 是否核心线程数。如果为true,则firstTast的边界是核心线程数(corePoolSize)否则是最大线程数(maximumPoolSize),此处使用boolean指示符而不是一个值以确保在检查其他线程状态后读取或者刷新值。
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 仅在必要时检查队列是否为空。
// 代码rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()如果为以下值
// true&&true&&true表示线程池执行器已经SHUTDOWN,没有新任务,线程池不为空,则下一步自旋
// false&&true&&true表示线程池没有SHUTDOWN,没有新任务,线程池不为空,则addworker返回false。不创建新任务。
if (rs >= SHUTDOWN &&//线程池处于非运行状态
! (rs == SHUTDOWN &&//线程池已经SHUTDOWN后,不能添加线程,直接拒绝
firstTask == null &&//没有任务
! workQueue.isEmpty()))//队列也不为空
return false;
for (;;) {//自旋,CAS +1
int wc = workerCountOf(c);//获取当前工作线程数
//如果工作线程数大于默认容量或者工作线程大于等于核心线程数或最大线程数,则返回false,即不创建工作线程。
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//调用AtomicInteger的cas进行增加工作线程数,如果失败,则跳转到重试标签。
if (compareAndIncrementWorkerCount(c))
break retry;
//否则成功增加工作线程数,则重新读取ctl(线程数量及状态)值。
c = ctl.get();
if (runStateOf(c) != rs)//再次判断运行状态是否等于之前获取的rs,如果不相等,说明其他线程已经执行了前面的CAS操作,则重试。
continue retry;
// 否则因为workerCount改变CAS失败,则不断循环重试。
}
}
//上面只对工作线程数wc加1的原子操作,下面构建一个worker
boolean workerStarted = false;//工作线程默认未启动
boolean workerAdded = false;//工作线程还未成功添加
Worker w = null;
try {
w = new Worker(firstTask);//用执行器的线程工厂新建一个Worker
final Thread t = w.thread;//从worker中取出线程
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();//用当前执行器的重入锁加锁,避免并发,可重入锁在其他章节具体说。默认是非公平的,采用cas,内部维持了一个等待队列节点。
try {
// 维持锁时还需要重新检查线程池执行器运行状态rs
// 在ThreadFactory失败或者在获取锁之前关闭时退出
int rs = runStateOf(ctl.get());
// 当rs小于0表示线程池正在运行或者(关闭且firstTask任务为null),才能添加到 workers 集合中
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // 再次检查线程是否存活,如果已经被启动,抛出异常
throw new IllegalThreadStateException();
workers.add(w);//将新建的worker添加到workers集合中
int s = workers.size();//再次判断工作线程数
if (s > largestPoolSize)//如果大于曾经最大线程数
largestPoolSize = s;//赋值给曾经最大线程数,即替换为新的曾经最大线程数
workerAdded = true;//标识为添加成功
}
} finally {
mainLock.unlock();//释放线程池重入锁
}
if (workerAdded) {//如果添加成功,则启动线程
t.start();//运行当前线程
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);//启动失败,递减实际工作线程数
}
return workerStarted;
}
当任务提交到Worker中,那Worker内部又是怎样的?接下来我们看下Worker类。
Worker类主要维持了运行任务的线程的中断控制状态。为了简化获取和释放锁的过程,Worker类继承了AbstractQueuedSynchronizer类。这可以防止唤醒等待任务的工作线程而不是中断正在运行的任务。这里并没有使用重入锁ReentrantLock,而是使用一个简单的不可重入互斥锁。因为当调用像setCorePoolSize之类的方法时并不希望线程池执行器需要重新获取锁,tryAcquire方法也与ReentrantLock的逻辑不一样,worker中是不允许重入的。另外在线程真正运行任务之前禁止中断,通过将锁状态初始化为负值,并在启动时清除它(在runWorker中)。
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable{
/**
* 这个类永远不需序列化,但提供serialVersionUID来压制javac编译器警告。
*/
private static final long serialVersionUID = 6138294804551838833L;
/** 正在运行的工作线程 */
final Thread thread;
/** 要运行的任务,可能为空 */
Runnable firstTask;
/** 每个线程完成任务的计数器 */
volatile long completedTasks;
// Lock methods
// 0表示未上锁状态
// 1代表已经上锁
/**
* 从工厂方法ThreadFactory创建给定的第一个任务和线程。
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
setState(-1); // 在调用runWorker方法前禁止中断
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
/**委托主运行循环方法给外部类的runWorker方法 */
public void run() {
runWorker(this);
}
}
接下来了解下ThreadPoolExecutor的runWorker方法。runWorker就是获取不为空的任务执行。大概逻辑如下。
- 如果task不为空,则开始执行task。
- 如果task为空,则通过getTask()获取任务,并且Runnable不为空,则执行该任务。
- 执行完毕后,通过while循环继续getTask()取任务。
- 如果getTask()取到的任务为空,则整个runWorker()方法执行完毕。
/**
*
* 主工作运行循环.从队列中不断获取任务执行,执行中处理以下问题。
* 1. 我们通过一个初始任务开始,在这种情况下,我们不需要获取第一个任务,否则,只要线程池在运行,我们可以通过getTask获取任务。如果它返回null然后由于池的状态发生变化或者配置参数变化。外部代码将抛出异常,在这种情况下,通常通过processWorkerExit来替代这个线程。
*
* 2. 在运行任何任务之前,获取锁是为了在任务执行期间防止池中线程被中断,然后我们确保池停止,否则此线程没有设置其中断。
*
* 3. Each task run is preceded by a call to beforeExecute, which
* might throw an exception, in which case we cause thread to die
* (breaking loop with completedAbruptly true) without processing
* the task.
*
* 4. Assuming beforeExecute completes normally, we run the task,
* gathering any of its thrown exceptions to send to afterExecute.
* We separately handle RuntimeException, Error (both of which the
* specs guarantee that we trap) and arbitrary Throwables.
* Because we cannot rethrow Throwables within Runnable.run, we
* wrap them within Errors on the way out (to the thread's
* UncaughtExceptionHandler). Any thrown exception also
* conservatively causes thread to die.
*
* 5. After task.run completes, we call afterExecute, which may
* also throw an exception, which will also cause thread to
* die. According to JLS Sec 14.20, this exception is the one that
* will be in effect even if task.run throws.
*
* The net effect of the exception mechanics is that afterExecute
* and the thread's UncaughtExceptionHandler have as accurate
* information as we can provide about any problems encountered by
* user code.
*
* @param w the worker
*/
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // 设置当前工作线程允许中断,因为new Worker默认的state = -1, 通过调用tryRelease()方法设置AQS状态为0
boolean completedAbruptly = true;
try {
//当前任务不为空 则进入 或者 能 从workQueue中获取take一个任务,并赋值给当前任务,再进入,否则队列中没有任务,退出
while (task != null || (task = getTask()) != null) {
w.lock();//上锁,防止在shutdown时不终止正在运行的worker
//如果线程池为stop,确保线程已经被中断
//(Thread.interrupted() &&runStateAtLeast(ctl.get(), STOP)确保线程中断标志位为true且是stop状态以上,接着清除了中断标志
//!wt.isInterrupted()则再一次检查保证线程需要设置中断标志位
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);//默认没实现,可重写用于定制自己的ThreadpoolExecutor,用来监控线程池状态
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);//默认没实现,可重写用于定制自己的ThreadpoolExecutor
}
} finally {
task = null;//置空任务
w.completedTasks++;//完成任务数+1
w.unlock();
}
}
completedAbruptly = false;
} finally {
//将入参worker从数组workers里删除
processWorkerExit(w, completedAbruptly);//销毁任务
}
}
接下来讨论下woker线程从阻塞队列中获取任务。
/**
* 根据当前的配置设置,执行阻塞或定时等待任务,如果此时因为以下情况必须退出,则返回null
* 1. 超过maximumPoolSize的线程 (通过setMaximumPoolSize设置).
* 2. 线程池状态为Stopped
* 3. 线程池已经关闭,且队列为空
* 4. 此工作线程在等待任务时超时,超时的工作线程将在超时等待之前或之后终止。如果队列不为空,则工作线程不是池中最后一个线程。
* @return 如果工作线程必须退出,则返回null,且workerCount 也相应递减
*/
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {//自旋
int c = ctl.get();
int rs = runStateOf(c);
// 以下两种情况返回空
// 1.如果线程池状态(为非运行状态)且workQueue为空,反映了shutdown状态的线程池还是要执行workQueue中的剩余任务
// 2.线程池状态为stop,(shutdownNow()会导致变成 STOP)
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();//减少workCount数量
return null;//返回空,当前线程退出
}
int wc = workerCountOf(c);
// 判断线程是否需要进行超时控制
// allowCoreThreadTimeOut默认为false,即默认核心线程不允许进行超时
// wc > corePoolSize,表示当前线程池的线程大于核心线程
// 默认情况下allowCoreThreadTimeOut为false,如果当前线程池大于核心线程,则需要进行超时控制
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 1. 线程数量超过maximumPoolSize,这种情况可能是运行时调用了setMaximumPoolSize修改了最大线程池大小,否则是不会超过最大线程池大小的。
// 2. timed && timedOut 如果为true,表示当前操作需要进行超时控制,并且上次从阻塞队列中获取任务发生超时,这里就是剔除已经到期的空闲线程
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))//cas减少WorkerCount数量
return null;
continue;
}
try {
//如果需要进行超时控制,则通过poll方法获取任务,在keepAliveTime内没有获取则返回null
//否则通过take方法阻塞式获取队列中的任务。
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)//如果获取得任务不为空,则直接返回该任务
return r;
timedOut = true;//否则设置超时标志位true,待下一循环回收
} catch (InterruptedException retry) {
//如果获取任务时当前线程被其他线程中断了,则设置timeOut为false,并返回循环重试
timedOut = false;
}
}
}
主要分为以下几个步骤。图有点问题,要强调任务和线程的区别。
- 当线程池中的线程数小于corePoolSize时,新提交的任务将创建一个新线程执行任务,即使此时线程池中有空闲线程。
-
当线程池中达到corePoolSize时,新提交的任务将会提交到工作队列workQueue中,等待线程池中的任务来调度执行。
-
当workQueue队列已满,且最大线程池maximumPoolSize大于corePoolSize时,新提交任务会创建新线程执行任务。
-
当提交的任务超过maxmumPoolSize+workQueue时,新提交的任务将由RejectedExecutionHandler#rejectedExecution根据拒绝策略处理。拒绝策略包括。
(1) AbortPolicy,直接抛出异常。
(2) DiscardPolicy,不做任何处理,直接抛弃任务。
(3) DiscardOldestPolicy:将队列中的头节点(最老的节点)出队抛弃,再尝试提交任务。如果此时使用PriorityBlockingQueue优先级队列,将会导致优先级最高的队列被丢弃,因此不建议此策略配置为优先级队列。
(4) CallerRunsPolicy:既不抛弃任务,也不抛弃异常,直接运行任务的run方法,也就是交给调用者,由调用者主线程自己来执行该任务。在主线程执行该任务期间因无法提交新任务,从而线程池在此期间有时间处理队列中的任务。
-
当线程池中的线程超过corePoolSize,空闲时间超过keepAliveTime的线程会被销毁。
-
当线程池中的线程数小于等于corePoolSize,并且还有空闲线程时,核心线程数中空闲的线程将会从workQueue中获取任务执行。
-
当设置allowCoreThreadTimeOut为true时,只要线程空闲超过keepAliveTime后就会被销毁,如下图。如果没有配置allowCoreThreadTimeOut为true,则核心线程数不会销毁。
线程池有哪些
为了方便使用线程池,在Executors中提供了几个线程池的工厂方法。其真正使用ThreadPoolExecutor的相关方法来实现线程池的管理。
- newFixedThreadPool:固定线程大小的执行器。可控制最大并发数,超出的线程将在队列中等待。
- newSingleThreadExecutor:创建只有单个worker线程的执行器,用唯一的工作线程来执行。
- newCachedThreadPool:可变线程的执行器,线程池大小超过需要,则回收;无回收,则新建线程。
- newScheduledThreadPool:线程延迟执行器,用于定时及周期性任务执行的场景。
newFixedTheadPool
固定线程池,指核心线程数=最大线程数。从以下源码可以,核心线程数和最大线程数相等,也就是说当线程池中超过核心线程数后,将直接将任务放入阻塞队列。且多余线程keepAliveTime保留时间为0,创建了一个无界的线性阻塞队列。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory{
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
- 创建重复使用固定数量线程的线程池
- 通过无边界队列进行操作。当需要时使用提供的ThreadFactory创建新线程。任何时候,最多nThreads个线程将处于活动状态。当所有线程都处于活动状态时,再提交新任务,新任务将在队列中等待直到某个线程可用。否则线程池空闲则立即执行。
- 如果任何线程由于在执行过程中失败而终止,如果需要执行随后的任务,一个新的线程将会取代之前失败的任务。
- 新的线程会一直存在池中,直到显示地调用ExecutorService#shutdown方法来关闭。
newSingleThreadExecutor
从源码可以看出,核心线程数和最大线程数都为1,且该线程永不回收,其他线程都将放到队列中排队。保证所有任务按指定顺序执行(FIFO,LIFO,优先级三种顺序)。
- 创建一个执行器,该执行器使用单个工作线程在无边界队列上操作。
- 在需要时使用提供的threadfactory创建新线程。
- 如果任何线程由于在执行过程中失败而终止,如果需要执行随后的任务,一个新的线程将会取代之前失败的任务。 任务被确保顺序执行。并且在任何时间都不会有多个任务处于活动状态。
- 与等价的newFixedThreadPool(1)不同,返回的执行器保证不可重新配置来使用附加的线程。
- 返回最新创建的单个线程执行器。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
以上两个ExecutorService都采用LinkedBlockingQueue无界队列。
newCachedThreadPool
从源码可以看出,核心线程数为0,最大线程数几乎无上限。每个线程空闲60s后被回收。若无空闲线程,则新建线程,采用SynchronousQueue队列。可以理解为线程个数为0~最大整数范围的一个可伸缩线程池。
- 根据需要创建新线程的线程池,如果之前的线程可用则重新使用它们。
- 这种类型的线程池通常能对很多短期的异步任务提高程序性能。
- 如果之前的创建的线程可用,则调用execute()可以重新使用。如果没有可用的线程存在,一个新的线程将会被创建并加入到线程池中。
- 如果60s没有使用的线程将会被中断并从cache中移除直到corePoolSize为0。因此,一个空闲时间足够长的线程池不会消耗任何资源。
- 可以使用ThreadPoolExecutor构造相同属性但是不同详细信息(如超时)的线程池。
- 这里使用了SynchronousQueue,意味着不会存储任务,当已经存在的线程都任务满时,每个新任务启动一个新线程去执行。当在高负载场景下,最多会出现线程"饥饿"的情况,但是最坏的情况是出现OutOfMemoryError。这个我们最好是系统维持控制,而不要出现客户对我们的系统进行"DDOS"攻击。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
newScheduledThreadPool
在给定延迟后创建一个可执行计划命令的线程池,或者周期性执行提交的任务。可理解为一个可定时执行任务的线程池。
corePoolSize为线程池中保持的数量,即使空闲也会保持。 maximumPoolSize=Integer.MAX_VALUE,workQueue=DelayedWorkQueue的线程池 。 DelayQueue是基于优先级队列来实现的,是一种无界延迟阻塞队列。threadFactory当executor创建线程时使用该工厂。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
//ScheduledThreadPoolExecutor类中的构造方法
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
关闭线程池方式
ThreadPoolExecutor提供了两种方式关闭线程池。
shutdown():不会立即终止线程池,而是要等所有缓存队列中的任务都执行完后才终止。
shutdownNow():立即终止线程池,并尝试打断提交内核正在执行的任务,并且清空缓存队列,返回没有被执行的任务。
等待队列
workQueue对象就是用来存放等待执行的任务的。
- ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小。
- LinkedBlockingQueue:基于链表的先进先出队列,创建时不必指定队列大小,默认为为Integer.MAX_VALUE。
- SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是直接新建一个线程来执行新的任务。
怎么选择线程池
线程池大小是需要根据系统业务量大小并通过压力测试确定的,下面方式只能算是估值,最终需要测试后对这些估值做微调。
Brian Goetz 在《并发编程实践》中推荐用下面的公式。
Number of threads = Number of Available Cores * (1 + Wait time / Service time)
Waiting time :是等待IO绑定的任务完成所花费的时间。例如等待远程服务的响应时间。
Service time :处理HTTP响应的时间。封装、解析等其他转换的时间。
Wait time / Service time :这个通常叫阻塞系数
计算密集型任务的阻塞系数接近0,在这种情况下,线程数可以设置等于可用内核数。如果所有的任务都是计算密集型,那更多的线程也无意。如:某个工作线程调用某个微服务,序列化为JSON和执行某些规则集,微服务的响应时间为50ms,处理时间为5ms,我们将我们的应用部署到双核CPU的服务器上。
2 * (1 + 50 / 5) = 22 // 可选的线程池大小
显然这个例子过于简单化,在这个服务上,可能除了HTTP连接外,应用程序可能还有来自JMS的请求,可能还有JDBC连接池。如果你有不同的任务类,最好使用多线程池,这样每个线程池都可以根据其工作负载进行优化。对于多个线程池,只需要向公式中添加一个目标CPU利用率参数。
目标CPU利用率为[0-1],1意味着线程拉取任务让CPU满负荷运行。公式就会变成:
Number of threads = Number of Available Cores * Target CPU utilization * (1 + Wait time / Service time)
在这一步,我们将得到一个最佳的线程池大小,前面我们通过一些指标可以得到理论的上界。但是,并行工作线程的数量如何改变延迟或吞吐量?我们可以采用利特尔法则(Little’s Law)进行评估。
Little’s Law,在一个稳定的系统L中,长期的平均顾客人数等于长期的有效抵达率( λ ),乘以顾客在这个系统中的平均的等待时间(W)。即:
L
=
λ
W
L=λW
L=λW
这个法则应用在Web应用领域可理解为:一个系统中的平均线程数=平均网络到达率*平均响应时间。其中平均网络到达率为平均每秒钟请求数。只要知道平均每秒的请求数λ乘以平均等待时间W就可以得到线程池大小L。
在我们想设定线程池大小之前,我们需要知道要加入线程池的服务如果是依赖于数据库,则线程池则依赖于数据库连接池大小。线程池中设置1000个线程来处理前端100个连接是无意义的。如果工作线程调用的是外部系统,且外部系统能并发处理很少的线程,那线程池就受限于这个外部服务。显然我们经常忘记这两个因素。
上面的例子中,我们有一个平均响应时间为55ms的服务(50等待时间+5服务时间),线程池大小为22个工作线程。我们可以使用等待理论公式计算系统容量或者我们需要多少个并行运行实例,以便在稳定的响应时间内处理每秒中内的请求数。
22 / 0.055 = 400 // the number of requests per second our service can handle with a stable response time
线程池很重要的资源就是CPU。在Java中我们可以通过下面的方法获取CPU总数。
int numOfCores = Runtime.getRuntime().availableProcessors();
在Linux操作系统下,可以使用命令查询逻辑CPU个数。
cat /proc/cpuinfo| grep "processor"| wc -l
虽然这是一种很经典的方式,但是在容器环境中要小心使用。没有特殊的限制,容器进程将会获取主机操作系统的硬件资源。关于这个话题,大家可以参考下这个链接 Better Containerized JVMs in JDK10和 Nobody puts Java in a container。
Web项目的线程池模型
我们现在的Web系统,大都是采用Springboot框架,内嵌Tomcat容器,数据外部来源主要有基于RestFul风格的Http访问、基于Dubbo的RPC方式的调用和数据库连接池获取数据库数据,当然你自己也可以DIY自己的线程池。所以,目前Web系统的线程池模型大概如下图所示。
TOMCAT线程池配置
TOMCAT线程池配置有两种方式,第一种是在TOMCAT_HOME/conf/server.xml中配置。第二种是在SpringBoot项目中嵌套时,在application.yml中配置。第一种方式如下所示。
(1)配置executor属性。重要参数说明如下。
<Executor name="tomcatThreadPool"
namePrefix="tomcatThreadPool-"
maxThreads="1000"
maxIdleTime="300000"
minSpareThreads="200"/>
name:共享线程池的名称。必须唯一,缺省值为:None。
namePrexfix:在JVM中,我们经常会在日志查找中会找到" http-nio-8080"字样。这个namePrexfix就是为线程池中的每个线程的name属性前加入一个前缀。而" http-nio-8080"正是tomcat中采用NIO运行模式namePrexfix的缺省值。
maxThreads:最大线程数。缺省值为200。
maxIdleTime:在Tomcat关闭某个空闲线程之前等待的毫秒数。当前活跃的空闲线程大于该值时才关闭该线程。缺省值60s。
minSpareThreads:Tomcat应该始终打开的最小不活跃线程数。缺省值25。
(2)配置Connector属性。重要参数如下。
executor:上一步中配置的Executor的name属性。
minProcessors:JVM启动时,创建的线程数。
maxProcessors:最大线程数。
acceptCount:当所有可使用的线程都用完时,可放入队列中的请求数。超过该值的请求将不予处理。
<Connector executor="tomcatThreadPool"
port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
redirectPort="8443"
minProcessors="5"
maxProcessors="75"
acceptCount="1000"/>
Dubbo线程池配置
Dubbo的底层是基于Netty的通讯框架。在Dubbo的调用过程中,有两个线程池,一个是IO线程池,一个是业务线程池。以下是Dubbo线程池调优的配置信息。
<dubbo:protocol name="dubbo" dispatcher="all" threadpool="fixed" threads="300" />
name,为协议名称。默认为dubbo协议。而dubbo默认传输协议为netty,序列化为hessian,当然也可以用json序列化。dubbo连接默认采用单一长连接和NIO异步通讯。
port,服务端口。dubbo默认端口为20880。
threads,服务线程数量。
iothreads,io线程池大小。默认为CPU个数+1。
queues,线程池队列大小。默认为0,即用SynchronousQueue队列,建议不要设置。
其中,threads默认为200(2.X.X以上版本)。dispatcher参数值有以下情况。
(1) all,所有消息都直接发送到业务线程池,包括请求,响应,连接/断开事件,心跳等。
(2) direct,所有消息都不派发给业务线程池,直接在IO线程上处理。如果该业务能快速响应,可以配置成该模式可以减少线程池的调度。
(3)message,只有业务请求响应派发到业务线程池,其他如连接,断开和心跳消息直接在IO线程上执行。
(4)execution,只有业务请求派发到业务线程池,业务的响应,连接,断开和心跳直接在IO线程上执行。
(5)connection,在IO线程上执行,将连接断开事件放入队列逐个执行,
threadpool有以下情况。
(1)fixed 固定大小线程池。启动时建立且一直不关闭。
(2)cached 缓存线程池,空闲1分钟关闭,需要时新建。
(3)limit 可伸缩线程池,业务线程池中的线程只增不减。
数据库线程池配置
数据库线程池配置有两种方式,第一种是采用 com.mchange.v2.c3p0.ComboPooledDataSource
来配置。而第二种方式是采用静态DataSources工厂类 com.mchange.v2.c3p0.DataSources
。详情请查阅c3p0官网。在实际操作中,我们很少直接采用这两种方式实现。而是采用JNID或直接在项目中配置DataSource的方式。
(1)采用JNID方式,如果采用JNID方式。配置线程池在Tomcat的server.xml中增加Resource资源就可以了。当然相对应的jar要拷贝到tomcat/lib下,web.xml,DataSource的获取都需要在项目中配置正确。下图是对应的server.xml的配置。
<Context docBase="mySystem" path="/mySystem"
reloadable="false" source="org.eclipse.jst.jee.server:ycloans">
<Resource auth="Container"
description="DB Connection"
driverClass="com.mysql.jdbc.Driver"
maxPoolSize="100"
minPoolSize="2"
acquireIncrement="2"
name="jdbc/mysqlds-c3p0"
user="root"
password=""
factory="org.apache.naming.factory.BeanFactory"
type="com.mchange.v2.c3p0.ComboPooledDataSource"
jdbcUrl="jdbc:mysql://localhost:3306/mydb1" />
</Context>
注意,不要与tomcat线程池混淆。这个配置的节点是在Context下的呢。
(2)采用springboot的自动注入方式。因上面采用C3P0连接池,下面换个口味,采用阿里的Druid连接池。
@Bean(name = "masterDataSource")
@Primary
public DataSource masterDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driverClass);
dataSource.setUrl(url);
dataSource.setUsername(user);
dataSource.setPassword(password);
dataSource.setDbType(type);
dataSource.setInitialSize(initialSize);
dataSource.setMinIdle(minIdle);
dataSource.setMaxActive(maxActive);
dataSource.setMaxWait(maxWait);
dataSource.setTimeBetweenConnectErrorMillis(timeBetweenEvictionRunsMillis);
dataSource.setValidationQuery(validationQuery);
dataSource.setTestWhileIdle(testWhileIdle);
dataSource.setTestOnBorrow(testOnBorrow);
dataSource.setTestOnReturn(testOnReturn);
dataSource.setPoolPreparedStatements(poolPreparedStatements);
return dataSource;
}
线上一个线程池的BUG
之前在生产环境出现了一个BUG,当时的表象就是数据库线程池满了。通过jstack命令将堆栈信息导出。通过查找我们发现。通过Dubbo连接进来,而Dubbo配置为缺省值,即dispatcher=“all”,所有请求发给业务线程池,所以,Tomcat容器接管该请求,而tomcat线程池配置的是type=“com.mchange.v2.c3p0.ComboPooledDataSource” ,也就是说tomcat中用的链接池是数据库连接池,而当时生产环境配置的链接是200,而数据库连接池为什么会一直不释放,原因是因为有个SQL语句,当时是没有加合适索引的,也就是不走索引。而此刻就积压了很多线程,一直在等待。系统反应很慢。所以,写SQL时必须要注意执行效率。
总结
-
线程池什么时候创建线程?线程池中的线程是否在创建线程池的时候就创建了?
在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务 。 调用prestartAllCoreThreads()或者prestartCoreThread()可以在创建时启动线程,但是一般都不需要这样做 。 当池中线程数量小于corePoolSize时,即使有线程空闲,线程池也会优先创建新线程处理,直至线程数量 = corePoolSize,
如果任务数量超过corePoolSize时,会先进入队列排队,直至队列存储已满,才会继续从非corePoolSize中创建线程,直至线程数量=maximumPoolSize,才会停止创建 。 -
线程池中的线程什么销毁?
当线程空闲时间超过keepAliveTime时,变回自动回收线程,直至线程数量 = corePoolSize,换句话说,corePoolSize中的线程永远不会被回收(除非制定了allowCoreThreadTimeOut(true),才会一并回收corePoolSize的线程),系统只会回收非corePoolSize的线程 。 -
系统中最多有几个正在运行的任务?最多可以等待几个待执行的任务?
最多运行maximumPoolSize个线程,等待的任务数量由队列长度决定,也就是说,队列无界时,那么可以存储任意个任务,直到系统资源耗尽为止 。