学习自:漫画:聊聊线程池中,线程的增长/回收策略
今天在微信推送时看到这么一篇文章,刚好回顾一下之前学习的线程池知识。
线程池
为了避免系统频繁的创建和销毁线程,我们可以将创建的线程进行复用。创建线程变成了从线程池获取空闲的线程,关闭线程变成了向池子中归还线程。
线程池的好处
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
第二:提高响应速度。当任务完成时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。
JDK
对线程池的支持在Java1.5
中提供了Executor
,可以让我们有效的管理和控制我们的线程,其本质也是线程池。
Java里面线程池的顶级接口是Executor,不过真正的线程池接口是ExecutorService
, ExecutorService
的默认实现是 ThreadPoolExecutor
;普通类 Executors 里面调用的就是 ThreadPoolExecutor
。
线程池策略
线程池中的参数意义
这里我们来重新认知一下线程池吧!
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
// ...
}
corePoolSize
:表示核心线程数量
maximumPoolSize
:表示最大线程数量
keepAliveTime
:表示核心线程数之外的线程,最大空闲存活的时长;
unit
:表示keepAliveTime
的时间单位;
workQueue
:表示线程池的任务等待队列;
threadFactory
:线程工厂,用来为线程池创建线程;可以用于给线程设置名字,一般默认不设置参数
handler
:饱和策略,当线程池无法处理任务时的拒绝方式;这是当任务对列和线程池都满时,采取的应对策略,默认是AbordPolicy
,表示无法处理新任务。并且抛出异常(RejectedExecutionException
)。
CallerRunsPolicy
:用调用者所在的线程来处理任务。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。DiscardPoliicy
:不能执行的任务,并将该任务删除。DiscardOldestPolicy
:丢弃队列最近的任务,并执行当前任务。
其中这里面的参数是互相影响的,例如workQueue
配置不正确,会导致maximumPoolSize
形同虚设,导致线程池中的线程,永远无法增长到核心线程数maximumPoolSize
配置的线程数。
线程池中的线程增长策略
线程池中的增长策略主要这三个参数有关。
corePoolSize
:核心线程数量
maximumPoolSize
:最大线程数量
workQueue
:线程池的任务等待队列
线程池的任务处理策略:
如果当前线程池中的线程数目小于corePoolSize
,则每来一个任务,就会创建一个线程去执行这个任务;
如果当前线程池中的线程数目>=corePoolSize
,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;如果当前线程池中的线程数目达到maximumPoolSize
,则会采取任务拒绝策略进行处理;
如果线程池中的线程数量大于corePoolSize
时,如果某线程空闲时间超过keepAliveTime
,线程将被终止,直至线程池中的线程数目不大于corePoolSize
;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime
,线程也会被终止。
这里可以参考流程图,消化上面文字
结合上图,可以看出,我们执行ThreadPoolExecutor
开始提交任务(execute
)时,遇见的各种情况
如果线程数没有带到核心线程数(
corePoolSize
),则创建核心线程数处理任务,
如果线程数大于或者等于核心线程数(
corePoolSize
),则将任务加入任务队列(workQueue
),线程池中的空闲线程会不断的从任务队列中取出任务进行处理.
如果任务队列满了,且线程数没有达到最大线程数(
maximumPoolSize
),则会创建非核心线程去处理任务.
如果线程数超过了最大线程数,则执行饱和策略
测试成果
我们来看一下这么一个线程池的创建代码吧!
public static ExecutorService newThreadPool() {
return new ThreadPoolExecutor(
30, 60,
60L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
我们可以看出,设置了核心线程为30个,最大线程数为60,60L, TimeUnit.MILLISECONDS,
是指核心线程数之外的线程,最大空闲存活的时长为60毫秒,其线程池的任务等待队列为LinkedBlockingQueue<Runnable>
。
该线程池的任务等待队列为LinkedBlockingQueue<Runnable>
,我们来看一下LinkedBlockingQueue<Runnable>
的源码。
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
从中可以看出,我们的LinkedBlockingQueue
是如果不传递参数的话,默认是创建一个无界队列。
而从我们的例子上看来,线程池的任务等待队列是一个无界队列,那就表明,我们的队列不会满,除非将系统内存就消耗殆尽。
线程池中线程的收缩策略
线程池中执行的任务,总有执行结束的时候。那么线程池当线程池中存在大量空闲线程时,也会有一定的收缩策略,来回收线程池中多余的线程。
线程池中线程的收缩策略,和以下几个参数相关:
corePoolSize
:核心线程数;
maximumPoolSize
:线程池的最大线程数;
keepAliveTime
:核心线程数之外的线程,空闲存活的时长;
unit
:keepAliveTime
的时间单位;
corePoolSize
和 maximumPoolSize
我们比较熟悉了,另外能够控制它的就是keepAliveTime
空闲存活时长,以及这个时长的单位。
当线程池中的线程数,超过核心线程数时。此时如果任务量下降,肯定会出现有一些线程处于无任务执行的空闲状态。那么如果这个线程的空闲时间超过了 keepAliveTime&unit
配置的时长后,就会被回收。
需要注意的是,对于线程池来说,它只负责管理线程,对于创建的线程是不区分所谓的「核心线程」和「非核心线程」的,它只对线程池中的线程总数进行管理,当回收的线程数达到 corePoolSize
时,回收的过程就会停止。
对于线程池的核心线程数中的线程,也有回收的办法,可以通过 allowCoreThreadTimeOut(true)
方法设置,在核心线程空闲的时候,一旦超过 keepAliveTime&unit
配置的时间,也将其回收掉。
public void allowCoreThreadTimeOut(boolean value) {
if (value && keepAliveTime <= 0)
throw new IllegalArgumentException("Core threads must have nonzero keep alive times");
if (value != allowCoreThreadTimeOut) {
allowCoreThreadTimeOut = value;
if (value)
interruptIdleWorkers();
}
}
allowCoreThreadTimeOut()
能被设置的前提是 keepAliveTime
不能为 0。
查缺补漏
等待队列还会影响拒绝策略
等待队列如果配置成了无界队列,不光影响线程数量从核心线程数向最大线程数的增长,还会导致配置的拒绝策略永远得不到执行。
因为只有在线程池中的工作线程数量已经达到核心线程数,并且此时等待队列也满了的情况下,拒绝策略才能生效。
核心线程数可以被「预热」
前面提到默认的情况下,线程池中的线程是根据任务来增长的。但如果有需要,我们也可以提前准备好线程池的核心线程,来应对突然的高并发任务,例如在抢购系统中就经常有这样的需要。
此时就可以利用 prestartCoreThread()
或者 prestartAllCoreThreads()
来提前创建核心线程,这种方式被我们称为「预热」。
对于需要无界队列的场景,怎么办?
需求是多变的,我们肯定会碰到需要使用无界队列的场景,那么这种场景下配置的 maximumPoolSize
就是无效的。
此时就可以参考 Executors 中 newFixedThreadPool()
创建线程池的过程,将 corePoolSize
和 maximumPoolSize
保持一致即可。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(
nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
此时核心线程数就是最大线程数,只有增长到这个数量才会将任务放入等待队列,来保证我们配置的线程数量都得到了使用。