多线程与并发-线程池-ThreadPoolExecutor
概述:
线程池这东西很常见,使用的原因更好的对线程进行管理,就是减少线程创建销毁的开销。
虽然有几个现成的创建方法,但很多公司都不建议使用,而要求一定要通过ThreadPoolExecutor来创建,明确运行规则,指定更合适的参数。
这是一个参数最全的重载方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
一共有7个参数
corePoolSize,核心线程数
当有任务进来的时候,如果当前线程数未达到corePoolSize个数,则创建核心线程。
特点:
- 当线程数未达到最大值的时候,新任务进来,即时有空闲线程也不会复用,仍然创建新的线程。
- 核心线程一般不会被销毁,即使处于空闲状态,但是可以通过allowCoreThreadTimeOut(boolean value) 设置为 true ,这时核心线程的空闲时间超过keepAliveTime设定的时间时会被销毁
maximumPoolSize,最大线程池大小
线程池的最大的大小。
当核心线程已满,并且阻塞队列已满时,将判断当前线程数量是否达到最大值,如果未达到则创建一个非核心线程加入线程池,当线程数量>corePoolSize的时候,如果有线程的空闲时间超过keepAliveTime的时候将被销毁
keepAliveTime,超时时间
就是上述两个参数中提到的空闲时间,也就是线程最大空闲时间,默认用于非核心线程,通过 allowCoreThreadTimeOut(boolean value) 方法设置后,也会用于核心线程。(当然线程池中并没有对线程进行标记是否为核心线程,当数量超过核心线程数时,任意线程只有空闲时间超过设定值就会被销毁)。
当设置为0时,直接销毁非核心线程
unit,时间单位
这个参数与keepAliveTime是一起的,设定的就是超时时间的时间单位,秒、分、时等。
workQueue,阻塞队列
当核心线程池没有空闲时,将把任务任务加入阻塞队列。这个参数对线程池执行策略有很大影响。
有界队列:队列长度有上限,当核心线程已满时,新任务加入阻塞队列,阻塞队列已满时将根据maximumPoolSize判断是否创建新的线程加入线程池,当达到最大值时,将根据拒绝策略处理后续的任务。
无界队列:队列没有上限,也就是意味着队列永远不会达到上限,也就是说永远不会创建非核心线程,任务的处理速度有限。同时新来任务可以无限的向队列中添加。(警惕任务队列无限堆积的风险)
名称 | 是否有界 | 结构 | 默认值长度 | 最大值 | 备注 |
---|---|---|---|---|---|
ArrayBlockingQueue | 有界 | 数组 | 初始化指定大小 | ||
LinkedBlockingQueue | 有界 | 链表 | Integer.MAX_VALUE | Integer.MAX_VALUE | 出队和入队使用两把锁来实现 |
PriorityBlockingQueue | 无界 | 数组,堆 | 11 | Integer.MAX_VALUE-8 | 支持优先级但不能保证同优先级元素的顺序 |
DelayQueue | 无界 | PriorityQueue | 11 | 创建元素时可以指定多久才能从队列中获取当前元素 | |
LinkedTransferQueue | 无界 | 链表 | CAS实现 | ||
LinkedBlockingDeque | 无界 | 链表 | 双向阻塞队列 | ||
SynchronousQueue | 队列(公平),栈(非公平) | 0 | 内部没有任何存放元素的能力 | CAS实现,对本人这有点像生成者消费者中锁的通信 |
threadFactory,线程工厂
指定线程池中线程的创建方式,用于实现生成线程的方式、定义线程名格式、是否后台执行等等,可以用 Executors.defaultThreadFactory() 默认的实现即可,也可以用 Guava 等三方库提供的方法实现,如果有特殊要求的话可以自己定义。它最重要的地方应该就是定义线程名称的格式,便于排查问题了吧。(可以把业务请求编号等信息作为线程名称)。
handler,拒绝策略
当没有空闲的线程(线程池达到最大值maximumPoolSize)处理任务,并且等待队列已满(当然这只对有界队列有效),再有新任务进来的话,就要做一些取舍了,而这个参数就是指定取舍策略的,有下面四种策略可以选择:
ThreadPoolExecutor.AbortPolicy//直接抛出异常,这是默认策略;
ThreadPoolExecutor.DiscardPolicy//直接丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy//丢弃队列最前面的任务,然后将新来的任务加入等待队列
ThreadPoolExecutor.CallerRunsPolicy//由线程池所在的线程处理该任务,比如在 main 函数中创建线程池,如果执行此策略,将有 main 线程来执行该任务
现有方法创建线程池(不提倡):
虽然并不提倡用 Executors 中的方法来创建线程池,但还是用他们来讲一下几种线程池的原理。
先看一下一部分源码(有部分重载方法没有展示,有threadFactory的参数重载方法):
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
newScheduledThreadPool
继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。关于周期调度的暂不做展开。
以上几种总结一下:
方法 | corePoolSize | maximumPoolSize | keepAliveTime | unit | workQueue | threadFactory | handler |
---|---|---|---|---|---|---|---|
newFixedThreadPool | nThreads | nThreads | 0L | TimeUnit.MILLISECONDS | LinkedBlockingQueue | threadFactory | |
newSingleThreadExecutor | 1 | 1 | 0L | TimeUnit.MILLISECONDS | LinkedBlockingQueue | threadFactory | |
newCachedThreadPool | 0 | Integer.MAX_VALUE | 60L | TimeUnit.SECONDS | SynchronousQueue | threadFactory |
以上由ThreadPoolExecutor实现,只是参数有所不同。
不推荐使用的原因:
- newFixedThreadPool
由于使用默认使用的是 LinkedBlockingQueue 作为等待队列,这是一个无界队列,这也是使用它的风险所在,除非你能保证提交的任务不会无节制的增长,否则不要使用无界队列,这样有可能造成等待队列无限增加,造成 OOM。 - newSingleThreadExecutor
与newFixedThreadPool类似,只是设定了线程池大小为1,理由同上。 - newCachedThreadPool
关键点就在于它使用 SynchronousQueue 作为等待队列,它不会保留任务,新任务进来后,直接创建临时线程处理,这样一来,也就容易造成无限制的创建线程,造成 OOM - newScheduledThreadPool
计划型线程池,可以设置固定时间的延时或者定期执行任务,同样是看线程池中有没有空闲线程,如果有,直接拿来使用,如果没有,则新建线程加入池。使用的是 DelayedWorkQueue 作为等待队列,这中类型的队列会保证只有到了指定的延时时间,才会执行任务