文章摘要:在业务系统中,线程池框架技术一直是用来解决多线程并发的一种有效方法。
在JDK中,J.U.C并发包下的ThreadPoolExecutor核心类是一种基于Executor接口的线程池框架,将任务提交和任务执行解耦设计,其中ExecutorService和其各种实现类提供了非常方便的方式来提交任务并获取任务执行结果,并封装了任务执行的全部过程。本文将深入解读并分析以ThreadPoolExecutor为核心的j.u.c包下Executor线程池框架的部分重要源代码,一步步带读者搞清楚JDK中线程池框架背后的设计理念和运行机制。
一、线程池的概念和定义
自己在接触线程池技术之前,“一直觉得在Java中有线程Thread对象,在业务需要的时候不断地创建线程出来不一样也能满足需求么?”,如果大家跟我上面的这个想法一样,不妨先来看下如下的内容。
在服务器端的业务应用开发中,Web服务器(诸如Tomcat、Jetty)需要接受并处理http请求,所以会为一个请求来分配一个线程来进行处理。如果每次请求都新创建一个线程的话实现起来非常简便,但是存在这样的严重问题:
随着业务量的增加,如果并发的请求数量非常多,但每个线程执行的时间很短,这样就会频繁的创建和销毁线程(包括涉及JVM的GC),如此一来会大大降低业务系统的效率。可能出现服务器在为每个请求创建新线程和销毁线程上花费的时间和消耗的系统资源要比处理实际的用户请求的时间和资源更多。
那么有没有一种解决方案可以使线程在执行完一个任务后,不被销毁,而是可以继续执行其他的任务呢?
这就是线程池的出现的原因了,其为线程生命周期的开销和资源不足问题提供了解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。实际应用中,上文所述Tomcat这样的Web服务器也是利用线程池机制来接收并处理大量并发的http请求,可以通过其server.xml配置文件中的Connect节点的maxThreads(最大线程数)/maxSpareThreads(最大空闲线程数)/minSpareThreads(最小空闲线程数)/acceptCount(最大等待队列数)/maxIdleTime(最大空闲时间)等参数进行线程池调优。
1.线程池的定义
线程池是一种多线程任务处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。这里引用wiki上面的一个图如下:
2.线程池使用的场景
(a)单个任务处理时间相对短
(b)需要处理的任务数量很大
3.线程池的主要作用
(a)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
(b)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
(c)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
二、从一个简单例子说起
在JDK 1.5后引入的Executor线程池框架的最大优点是把任务的提交和执行解耦。要执行任务的人只需把Task描述清楚,然后提交即可。这个Task任务是怎么被执行的,被谁执行的,什么时候执行的,提交的人就不用关心了。在最上面的Executor接口中定义了execute方法,该方法接收Runnable类型的任务命令,对用户屏蔽底层线程的实现与调度细节,这是一种典型命令设计模式的应用。如下为一段非常简单的线程池代码例子:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
fixedThreadPool.execute(newRunnable() {
@Override
publicvoid run() {
logger.info("thetask is running")
}
});
在上面的例子中,生成线程池采用了工具类Executors的静态方法,设置了线程池中核心线程数和最大线程数均为5, 线程池中超过核心线程数目的空闲线程最大存活时间为0,同时使用LinkedBlockingQueue这样子的无界阻塞任务队列。除了newFixedThreadPool可以生成线程数固定大小的线程池,newCachedThreadPool可以生成一个可缓存且自动回收的线程池,newSingleThreadScheduledExecutor可以生成一个单个线程的线程池。newScheduledThreadPool还可以生成支持周期任务的线程池。
三、ThreadPoolExecutor线程池源码剖析
JDK中的Executor线程池框架是一个根据一组执行策略调用、调度、执行和控制线程的异步任务框架,其目的是提供一种将“任务提交”与“任务运行”分离开来的机制。作为Java线程池框架中继承Executor接口最为核心的类—ThreadPoolExecutor,有必要对其源代码进行深入分析。因此,本节以ThreadPoolExecutor的源代码举例,先对以Executor接口为核心的类结构图进行一个较为全面的展示,然后回归到源代码中,对线程池中任务如何提交、如何执行任务等方面分别进行阐述。为了控制篇幅,突出主要逻辑,文章中引用的代码片段去掉了非重点部分。
1.J.U.C线程池框架类图
从上面的类结构图中可以看出,在JDK的J.U.C包下面主要包含了三个接口,分别是:
(a)Executor:一个运行新任务的简单接口;
(b)ExecutorService:扩展了Executor接口,增加了一些用来管理线程池状态和任务生命周期的方法以及支持Future返回值任务提交的方法;
(c)ScheduledExecutorService:扩展了ExecutorService,增加了定期和延迟任务执行的方法;
这三个接口定义了JDK中Executor线程池框架的标准行为,这三个接口的具体代码可以参考JDK的代码。限于篇幅,这里就不对各个方法进行一一详细叙述了。类图中AbstractExecutorService类是一个抽象类,提供了线程池框架的一些模板方法,具体实现由其子类,ThreadPoolExecutor和ScheduledThreadPoolExecutor分别实现。Executors是个工具类,里面提供了很多静态方法,根据用户的需求选择返回不同的线程池实例。
对于类结构图中左边半部分定义了ThreadPoolExecutor核心类的成员变量,包括创建线程的类工厂—ThreadFactory/DefaultThreadFactory,用以描述具体任务执行线程的内部类—Worker(其继承AQS框架和Runnable接口)和提供线程池工作饱和策略的—RejectedExecutionHandler。
2.线程池的状态
线程有五种状态:新建、就绪、运行、阻塞、死亡。对于线程池来说,同样也具有五种状态:RUNNING, SHUTDOWN, STOP, TIDYING, TERMINATED。线程池状态转换图如下:
在具体说说线程池的五种状态之前有必要结合ThreadPoolExecutor核心类的代码进行一些分析,在该类的代码中对于线程池的五种状态定义如下:
private final AtomicInteger ctl = newAtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS =Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
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;
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) {return rs | wc; }
对于上面代码中定义的常量ctl是对线程池的运行状态和线程池中有效线程的数量进行控制的一个32位字段,它包含两部分的信息:其中高三位表示线程池的运行状态 (runState) ,低29位表示线程池内有效线程的数量 (workerCount)。COUNT_BITS 就是29,CAPACITY就是1左移29位减1(29个1),这个常量表示workerCount的上限值,大约是5亿。另外这里还定义了三个静态方法分别为,runStateOf—获取运行状态;workerCountOf—获取活动线程数;ctlOf—获取运行状态和活动线程数的值。
(a)RUNNING:处于RUNNING状态的线程池能够接受新任务,以及对新添加的任务进行处理。
(b)SHUTDOWN:处于SHUTDOWN状态的线程池不可以接受新任务,但是可以对已添加的任务进行处理。
(c)STOP:处于STOP状态的线程池不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
(d)TIDYING:当所有的任务已终止,ctl记录的"任务数量"为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
(e)TERMINATED:线程池彻底终止的状态。
3.创建Executor线程池方法
在上文第二节内容中,已经给出了一个创建简单线程池的例子,其中调用了JDK的ThreadPoolExecutor核心类的构造函数来创建的线程池实例。在这一节内容中,通过分析ThreadPoolExecutor核心类的构造函数以及参数来看下如何创建一个Executor线程池以及在创建时候需要关注哪些要素?
public ThreadPoolExecutor(intcorePoolSize,
intmaximumPoolSize,
longkeepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactorythreadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize||
keepAliveTime < 0)
throw newIllegalArgumentException();
if (workQueue == null || threadFactory== null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime =unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
(a)corePoolSize:该参数指的是线程池中核心线程的数量。当提交一个任务时,线程池会新建一个线程来执行任务,直到当前线程数等于corePoolSize。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程
(b)maximumPoolSize:从参数名称上也应该可以明白它的意思。该参数指的是线程池中允许的最大线程数。线程池的阻塞队列满了之后,如果还有任务提交,如果当前的线程数小于maximumPoolSize,则会新建线程来执行任务。这里有必要说明的是,如果使用的是无界队列,该参数也就没有什么效果了。
(c)keepAliveTime:该参数为线程空闲的时间。线程的创建和销毁是需要代价的。线程执行完任务后不会立即销毁,而是继续存活一段时间:keepAliveTime。默认情况下,该参数只有在线程数大于corePoolSize时才会生效。
(d)unit:该参数用于表示keepAliveTime的单位。
(e)workQueue:该参数用来表示保存线程池中等待执行任务的阻塞队列,等待的任务需要实现Runnable接口。我们可以选择这几种:ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO;LinkedBlockingQueue:基于链表结构的无界阻塞队列(如果设置初始化时的队列大小),FIFO;SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作,反之亦然;PriorityBlockingQueue:具有优先级别的阻塞队列。这里的几种阻塞队列都为JDK中J.U.C并发包下较为经典的阻塞队列,其源码值得阅读和学习,有兴趣的朋友可以自己阅读。
(f)threadFactory:该参数用于设置线程池中创建工作线程的工厂对象。该对象可以通过Executors.defaultThreadFactory()返回。
(g)handler:该参数为线程池的拒绝策略。所谓拒绝策略,是指将任务添加到线程池中时,线程池拒绝该任务所采取的相应策略。当向线程池中提交任务时,如果此时线程池中的线程已经饱和了,而且阻塞队列也已经满了,则线程池会选择一种拒绝策略来处理该任务。线程池提供的拒绝策略主要有以下四种(位于上面J.U.C线程池的类结构图中左半部分):
AbortPolicy:直接抛出异常,默认的线程池拒绝策略;
CallerRunsPolicy:用调用者所在的线程来完成待执行的任务;
DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
DiscardPolicy:直接丢弃任务;
下面这个图为线程池的逻辑结构图:
由上面的逻辑结构图可以初步知道线程池主要的执行流程:
(a)当有任务进入时,线程池创建线程去执行任务,直到核心线程数满为止;
(b)核心线程数量满了之后,任务就会进入一个缓冲的任务队列中;
1、当任务队列为无界队列时(诸如LinkedBlockingQueue队列,通过构造函数来配置为无界队列),任务就会一直放入缓冲的任务队列中,不会和最大线程数量进行比较。
2、当任务队列为有界队列时(诸如ArrayBlockingQueue队列),任务先放入缓冲的任务队列中,当任务队列满了之后,才会将任务放入线程池,此时会与线程池中最大的线程数量进行比较,如果超出了,则默认会抛出异常进行拒绝动作。否则,线程池才会执行任务。当任务执行完,又会将缓冲队列中的任务推入线程池中,然后重复此操作。
以下是线程池采用有界队列来处理任务的主要流程如下图所示:
4.线程池中任务提交与执行
本节将介绍线程池中最为核心的任务提交代码流程。Executor线程池可以根据业务需求的不同提供两种方式提交任务:Executor.execute()、ExecutorService.submit()。在实际应用中,我们可以使用execute方法来提交没有返回值的任务。因为没有返回值,所以实际也没有办法知道任务是否被线程池中的线程执行成功。通过以下的示例代码可以提交一个没有返回值的任务:
threadpool.execute(newRunnable() {
@Override
public void run() {
//TODO
//logical code here
}
});
另外,我们也可以通过submit方法来提交带有返回类型future的任务,通过这个返回值可以判断任务是否已经成功执行。通过future的get方法可以获取任务执行的返回值,这里需要说明的是get方法是阻塞的,直到任务返回为止,也可以通过get(long
timeout,TimeUnit unit方法)设置超时时间,避免一直阻塞等待。以下为submit方法提交任务的示例代码:
Futurefuture = executor.submit();
try{
Object ret = future.get();
}catch(InterruptedExceptione1){
//处理中断异常
}catch(ExecutionExceptione2){
//处理无法执行任务异常
}finally{
//最后关闭线程池
executor.shutdown();
}
限于篇幅,本文仅对ThreadPoolExecutor核心类的execute方法的实现进行深入分析,而对submit方法则不做分析阐述,感兴趣的朋友可以自行按照同样的方法进行分析(其实,通过看submit的源代码可以发现,它实际上还是调用的execute方法,只不过它利用了Future来获取任务执行结果)。execute方法的具体代码如下:
publicvoid execute(Runnable command) {
if (command == null)
throw new NullPointerException();
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);
}
从以上的ThreadPoolExecutor类的execute方法的实现代码中可以得出以下几步主要的执行流程:
Step1:如果线程池当前线程数小于corePoolSize,则调用addWorker创建新线程执行任务,且方法返回,如果调用失败执行Step2;
Step2:如果线程池处于RUNNING状态,则尝试加入待执行任务的阻塞队列;如果加入该队列成功,则尝试进行Double Check;如果加入失败,则执行Step3;
Step3:如果线程池当前为非RUNNING状态或者加入阻塞队列失败,则尝试创建新线程直到maxPoolSize;如果失败,则调用reject()方法执行相应的饱和拒绝策略;
这里需要注意的是,在Step2中如果加入队列成功,则会进行一个双重校验的过程。其主要目的是判断加入到阻塞队列中的任务是否可以被执行。如果线程池不是RUNNING状态,则调用remove()方法从阻塞队列中删除该任务,然后调用reject()方法处理任务。否则需要确保还有线程执行。
在上面的executor方法中多次调用了addWorker方法,可能已经有同学在默默关注这个方法了。addWorker方法的主要工作是在线程池中创建一个新的线程并执行,firstTask参数用于指定新增的线程执行的第一个任务,core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSize,false表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize。下面我们主要来看下这个addWork方法里面的具体实现代码:
privateboolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
//获取当前线程池的状态
int rs = runStateOf(c);
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
//内层循环,worker + 1
for (;;) {
//线程数量
int wc = workerCountOf(c);
//如果当前线程数大于线程最大上限CAPACITY return false
//若core ==
true,则与corePoolSize比较,否则与maximumPoolSize,大于returnfalse
if (wc >= CAPACITY ||
wc >= (core ?corePoolSize : maximumPoolSize))
return false;
//worker + 1,成功跳出retry循环
if(compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
//如果状态不等于之前获取的state,跳出内层循环,继续去外层循环判断
if (runStateOf(c) != rs)
continue retry;
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//新建线程:Worker
final ReentrantLock mainLock =this.mainLock;
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
mainLock.lock();
try {
int c = ctl.get();
int rs = runStateOf(c);
// rs< SHUTDOWN ==>线程处于RUNNING状态
// 或者线程处于SHUTDOWN状态,且firstTask
== null(可能是workQueue中仍有未执行完成的任务,创建没有初始任务的worker线程执行)
if (rs < SHUTDOWN ||
(rs == SHUTDOWN&& firstTask == null)) {
//当前线程已经启动,抛出异常
if(t.isAlive()) // precheck that t is startable
throw newIllegalThreadStateException();
workers.add(w);
//设置最大的池大小largestPoolSize,workerAdded设置为true
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;
}
如果对Executor线程池本身就有所了解的同学可能可以以上面的代码加上注释就可以明白addWorker方法中的具体含义了。但是这里仍然有必要再说下,在上面的addWorker方法的代码中,主要完成了以下几步流程:
Step1.首先,获取线程池的状态后先进行条件的判断,如果rs >= SHUTDOWN,则表示此时不再接收新任务;其次,判断以下三个条件只要有一个不满足则addWorker方法返回false。这三个条件分别为:
(a)rs == SHUTDOWN,此时表示线程池处于关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务;
(b)firsTask为空;
(c)阻塞队列不为空;
这里,首先考虑rs == SHUTDOWN的情况,在这种情况下不会接受新提交的任务,所以在firstTask不为空的时候会返回false;其次,如果firstTask为空,并且workQueue也为空,则返回false,因为队列中已经没有任务了,不需要再添加线程了。
Step2.这里先获取线程数,根据addWorker方法的第二个参数为true或者false进行判断。如果为true表示根据corePoolSize来比较,如果为false则根据maximumPoolSize来比较。然后,通过CAS进行worker + 1。
Step3.获取主锁mailLock,随后再次判断线程池的状态。如果线程池处于RUNNING状态或者是处于SHUTDOWN状态且 firstTask == null,则向线程池中添加线程,然后释放主锁mainLock并启动线程,最后return true。如果中途失败导致workerStarted= false,则调用addWorkerFailed()方法进行处理。这里需要注意的是,t.start()这个语句,启动时会调用Worker类中的run方法,Worker本身实现了Runnable接口,所以一个Worker类型的对象也是一个线程。
仔细的同学会发现上面addWorker方法的代码中还有一个Worker对象,在线程池中每一个线程被封装成一个Worker对象,线程池中维护的其实就是一组Worker对象,那么我们来看一下Worker的定义:
privatefinal class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
//省略代码......
/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
/**Per-thread task counter */
volatile long completedTasks;
/**
* Creates with given first task and threadfrom ThreadFactory.
* @param firstTask the first task (null ifnone)
*/
Worker(Runnable firstTask) {
setState(-1); // inhibit interruptsuntil runWorker
this.firstTask = firstTask;
this.thread =getThreadFactory().newThread(this);
}
/** Delegates main run loop to outerrunWorker */
public void run() {
runWorker(this);
}
//省略代码......
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { returnisHeldExclusively(); }
//省略代码......
}
内部Worker类继承了AQS,并实现了Runnable接口,其中firstTask用它来保存传入的任务;thread是在调用构造方法时通过ThreadFactory来创建的线程,是用来处理任务的线程。在调用构造方法时,需要把任务传入,这里通过getThreadFactory().newThread(this);来新建一个线程,newThread方法传入的参数是this,因为Worker本身继承了Runnable接口,也就是一个线程,所以一个Worker对象在启动的时候会调用Worker类中的run方法。同时,该类继承了AQS框架,使用其来实现独占锁的功能。这里可能有同学会问为什么不使用ReentrantLock来实现呢?可以看到tryAcquire方法,它是不允许重入的,而ReentrantLock是允许重入的。这里,之所以设置为不可重入,是因为不希望任务在调用类似像setCorePoolSize这样的线程池控制方法时重新获取锁,而去中断正在运行的线程。
讲到这里还没有看到线程池任务运行的代码,对Java线程Runnable接口比较熟悉的同学可能知道应该是在上面的run方法来执行具体的任务运行,那么下面我们再进一步的看下runWorker方法里面究竟是干什么的?
finalvoid runWorker(Worker w) {
Thread wt = Thread.currentThread();
//获取第一个任务
Runnable task = w.firstTask;
w.firstTask = null;
//允许中断
w.unlock(); // allow interrupts
//是否因为异常退出循环
boolean completedAbruptly = true;
try {
//如果task为空,则通过getTask来获取任务
while (task != null || (task =getTask()) != null) {
w.lock();
// If pool is stopping, ensurethread is interrupted;
// if not, ensure thread is notinterrupted. This
// requires a recheck in secondcase to deal with
// shutdownNow race while clearinginterrupt
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 newError(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w,completedAbruptly);
}
}
这里总结一下runWorker方法的执行过程:
Step1.外层循环不断地通过getTask()方法获取任务,其中getTask()方法从阻塞队列中取任务;
Step2.如果线程池当前正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态;
Step3.调用task.run()去执行任务,这里就是真正去执行任务了;
Step4.如果task为null则跳出循环,执行processWorkerExit()方法;
上面代码中的getTask()方法是用于从阻塞队列中获取任务的;processWorkerExit()方法则进行收尾工作,统计完成的任务数目并从线程池中移除一个工作线程以销毁工作线程。讲到这里基本可以总结下整个工作线程的生命周期:从execute方法开始,Worker使用ThreadFactory创建新的工作线程,runWorker通过getTask获取任务,然后执行任务,如果getTask返回null,进入processWorkerExit方法,整个线程结束。执行流程图如下:
限于篇幅所限,对于ThreadPoolExecutor核心类中的tryTerminate、shutdown、shutdownNow和interruptIdleWorkers等方法就不再在此进行赘述了。感兴趣的同学可以自己再阅读这几个类的源代码。
四、如何合理应用线程池
一般在我们自己的Spring-boot工程中都用如下的配置方式来注入线程池的Bean(Spring-MVC的工程是基于XML,配置大同小异)。不注意其中细节的同学觉得这里的几个参数按照自己的感觉随便设置即可,如果是自己做练习或者一些demo样例的话,确实是无所谓,但是要应用于生产实际环境的话,还是需要经过一番分析和思考来设置线程池的参数,才能满足业务需求达到优化系统性能的目标。
@Bean(name= "taskAsyncPool")
public Executor taskAsyncPool() {
ThreadPoolTaskExecutor executor = newThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setAllowCoreThreadTimeOut(allowCoreThreadTimeOut);
executor.setKeepAliveSeconds(keepAliveSeconds);
executor.setThreadNamePrefix("ExecutorThread-");
// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
executor.setRejectedExecutionHandler(newThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
要合理地应用线程池技术,就需要首先对任务的特性有足够的了解,可以从以下的几个方面来分析:
(a)任务性质:提交的任务到底是CPU密集型,IO密集型还是兼有两者的混合型;
(b)任务优先级:任务是否具备低、中、高的优先级;
(c)任务执行时间:任务执行时间需要较长、中、还是较短;
(d)任务依赖性:是否依赖其他系统,比如数据库连接;
首先,对于(a)我们可以根据任务性质的不同用不同规模的线程池来进行处理。CPU密集型的任务可以尽可能配置核心线程数较小的线程池,如配置Ncpu+1个线程的线程池。而对于IO密集型的任务由于任务长时间处于IO等待中,因此可以配置较多的线程,如2*Ncpu线程的线程池。如果对于混合型则通过适当地将其拆分成CPU密集型和IO密集型的分别处理。如果编程者不确定当前机器的CPU核数,JDK提供了Runtime.getRuntime().availableProcessors()方法进行获取。
对于(b)可以通过J.U.C并发包中的优先级队列—PriorityBlockingQueue来进行任务处理。它可以让高优先级的任务先执行,低优先级任务延迟执行。
对于(c)(d)可以根据任务不同执行时间,分别建立不同规模类型的线程池来进行任务处理。
另外,需要说明的是在实际应用中,还是建议在线程池中设置有界队列来初始化。因为有界队列可以增加系统的稳定性和预警设置,如果遇到线程池中线程数和任务队列均满的情况,可以直接执行饱和拒绝策略并抛出异常告警。若是采用了无界队列,则线程池中的任务队列的任务数目会积压得越来越多,最后撑爆服务器的内存,造成整个系统不可用。
五、总结
本文从线程池的概念和定义出发,简单介绍了其使用场景和主要作用。然后从一个简单的线程池样例说起,阐述了Executor线程池框架的任务的提交和执行解耦机制。通过给出J.U.C包下与线程池相关的类结构图,简要介绍了以ThreadPoolExecutor为核心的相关接口和类,并结合代码给出线程池几种状态定义以及转换图,详细描述了创建线程池方法的ThreadPoolExecutor构造函数。最后,以ThreadPoolExecutor类的execute方法为入口深入分析addWorker和runWorker核心方法的源代码,梳理了线程池整体工作原理、生命周期和运行机制。在向线程池提交任务时,除本文叙述的execute方法外,还有一个submit方法,submit方法会返回一个Future对象用于获取返回值,限于篇幅,有关Future和Callable将在其他篇幅中进行介绍。限于笔者的才疏学浅,对JDK的Executor线程池可能还有理解不到位的地方,如有阐述不合理之处还望留言一起探讨。