线程池的重要属性

 

一个线程池的核心参数有很多,每个参数都有着特殊的作用,各个参数聚合再一起后将完成整个线程池的完整工作。其中的六个尤为重要:线程状态和工作线程的数量,核心线程数和最大线程数,创建线程的工厂,缓存任务的阻塞队列,非核心线程存活的时间和拒绝策略。

 

线程状态和工作线程数量

 

首先线程池是有状态的,在不同的状态下,线程池的行为是不一样的。

 

然后线程池肯定是需要线程去执行具体的任务,所以在线程池中就封装了一个内部类Worker作为工作线程,每个Worker中都维持着一个Thread。

 

线程池的重点之一,就是控制线程资源合理高效的使用,所以必须控制工作线程的个数,所以需要保存当前线程池中工作线程的个数。

 

看到这里,你是否觉得需要用两个变量来保存线程池的状态和线程池中工作线程的个数呢?但是在ThreadPoolExecutor中只用了一个AtomicInteger型的变量就保存了这两个属性的值,那就是ctl。

ctl是一个原子操作类型(AtomicInteger)的变量。ctl的高3位用来表示线程池的状态(runState),低29位用来表示工作线程的个数(workerCnt)。为什么要用3位来表示线程池的状态呢,原因是因为线程池一共有5种状态,而2位只能表示出4种情况(2位是2^2,最多产生4种结果),至少需要3位才能表示得了全部的5种状态(3位是3^2,最多产生9种结果)。

核心线程数和最大线程数

 

现在有了标识工作线程的个数的变量了,那到底该有多少个线程才合适呢?线程多了会浪费线程资源,少了又不能发挥线程池的性能。

 

为了解决这个问题,线程池设计了两个变量来协作,分别是:

 

核心线程数(corePoolSize):用来表示线程池中的核心线程的数量,也可以称为可闲置的线程数量。

 

最大线程数(maximumPoolSize):用来表示线程池中最多能够创建的线程数量。

 

现在我们有一个疑惑,既然已经有了标识工作线程的个数的变量了,为什么还要有核心线程数和最大线程数呢?

 

其实你这样想就能够理解了,创建线程是有代价的,不能每次要执行一个任务时就创建一个线程,但是也不能在任务非常多的时候,只有少量的线程在执行,这样任务是来不及处理的,而是应该创建合适的足够多的线程来及时地处理任务。

 

随着任务数量的变化,当任务数量明显减少时,原本创建的多余的线程就没有必要再存活着了,因为这时使用少量的线程就能够处理得过来了,所以说真正工作的线程的数量,是随着任务的变化而变化的。

 

那核心线程数和最大线程数和工作线程个数的关系是什么呢?

工作线程的个数可能从0到最大线程数之间变化,当执行一段时间之后可能维持在核心线程数(corePoolSize),但也不是绝对的,取决于核心线程是否允许被超时回收。

 

创建线程的工厂

 

既然是线程池,那自然少不了线程。线程该如何来创建呢?这个任务就交给了线程工厂ThreadFactory来完成。

 

缓存任务的阻塞队列

 

上面我们说了核心线程数和最大线程数,并且也介绍了工作线程的个数是在0和最大线程数之间变化的。但是不可能一下子就创建了所有线程,把线程池装满,而是有一个过程:

 

当线程池接受到一个任务时,如果工作线程数没有达到corePoolSize,那么就会新建一个线程,并绑定该任务,知道工作线程的数量达到corePoolSize前都不会重用之前创建的线程。

 

当工作线程数达到corePoolSize了,这是又接收到新任务时,会将任务存放在一个阻塞队列(workQueue)中等待核心线程去执行。为什么不直接创建更多的线程来执行新任务呢?原因是核心线程中很可能已经有线程执行完自己的任务了,或者有其他线程马上就能处理完当前的任务,并且接下来就能投入到新的任务中去,所以阻塞队列是一种缓冲机制,给核心线程一个机会让他们充分发挥自己的能力。另外一个值得考虑的原因是,创建线程毕竟是代价昂贵的,不可能一有任务要执行就去创建一个新的线程。

 

所以我们需要为线程池配备一个阻塞队列,用来临时缓存任务,这些任务将等待工作线程来执行。

非核心线程存活时间

 

上面我们说了,当工作线程数达到corePoolSize时,线程池会将新接收到的任务放在阻塞队列中,而阻塞队列又分为两种情况:一种是有界的队列,一种是无界的队列。

 

如果是无界队列,那么当核心线程都在忙时,所有新提交的任务都会被存放在该无界队列中,这时最大线程数将变得没有意义,因为阻塞队列不会存在被装满的情况。

 

如果是有界队列,那么当阻塞队列中装满了等待执行的任务,这时再有新任务提交时,线程池就需要创建新的临时线程来处理,相当于增派人手来处理任务。

 

但是创建的临时线程是有存活时间的,不可能让它们一直都存活着,当阻塞队列中的任务被执行完毕,并且又没有那么多新任务被提交时,临时线程就需要被回收销毁,而在被回收销毁之前等待的这段时间,就是非核心线程的存活时间,也就是keepAliveTime属性。

 

那么什么是非核心线程呢?是不是先创建的线程就是核心线程,后创建的就是非核心线程呢?

 

其实核心线程跟创建的先后没有关系,而是跟工作线程的个数有关,如果当前工作线程的个数大于核心线程数,那么所有的线程都可能是非核心线程,都有被回收的可能。

 

一个线程执行完一个任务后,会去阻塞队列里面取新的任务,在取到任务之前,它就是一个闲置的线程。

 

取任务的方法有两种,一种是通过take()方法一直阻塞直到取出任务,另一种是通过poll(keepAliveTime, timeUnit)方法在一定时间内取出任务或者超时,如果超时这个线程就会被回收,请注意核心线程一般不会被回收。

 

那么怎么保证核心线程不会被回收呢?还是跟工作线程的个数有关,每一个线程在取任务的时候,线程池会比较当前的工作线程个数与核心线程数。

 

1.如果工作线程数小于当前的核心线程数,则使用第一种方法取任务,也就是没有超时回收,这时所有的工作线程都是核心线程,它们不会被回收。

 

2.如果工作线程数大于核心线程数,则使用第二种方法取任务,一旦超时就回收,所以并没有绝对的核心线程,只要这个线程没有在存活时间内取到任务去执行就会被回收。

 

所以每个线程如果想要保住自己核心线程的身份,必须充分努力,尽可能快得获取到任务去执行,这样才能避免被回收的命运。

 

核心线程一般不会被回收,但是也不是绝对的,如果我们设置了允许核心线程超时被回收的话,那么就没有核心线程这种说法了,所有的线程都会通过poll(keepAliveTime, timeUnit)来获取任务,一旦超时获取不到任务,就会被回收,一般很少会这样来使用,除非该线程池需要处理的任务非常少,并且频率也不高,不需要将核心线程一直维持着。

 

拒绝策略

 

虽然我们有了阻塞队列来对任务进行缓存,从一定程度上为线程池的执行提供了缓冲期,但是如果是有界的阻塞队列,那就存在队列满的情况,也存在工作线程的数据已经达到最大线程数的时候。如果这时候再有新的任务提交时,显然线程池已经心有余而力不足了,因为既没有空余的队列空间来存放该任务,也无法创建新的线程来执行该任务了,所以这时我们就需要有一种拒绝策略,即handler。

 

拒绝策略是一个RejectedExecutionHandler类型的变量,用户可以自行指定拒绝的策略,如果不指定的话,线程池将使用默认的拒绝策略:抛出异常。

 

在线程池中还为我们提供了很多其他可以选择的拒绝策略:

 

1.直接丢弃该任务

 

2.使用调用者线程执行该任务

 

3.丢弃任务队列中的最老的一个任务,然后提交该任务