ThreadPoolExecutor参数以及原理图解
参数释义
-
corePoolSize,
控制核心线程池的大小
-
maximumPoolSize
线程池最大线程数量
-
keepAliveTime
当某个线程超过生存时间(没任务待执行)后则会回归还操作系统;
-
TimeUnit.SECONDS
keepAliveTime 的单位;
-
BlockingQueue
任务队列,用于存放Runnable即任务
-
ThreadFactory
用于创建线程,以及指定线程的前缀(一般用于定位问题)和group以及是否为守护线程(优先级最低的,默认是非守护线程)
什么是守护线程??? 有时候,你希望创建一个线程来执行一些辅助工作,但又不希望这个线程阻碍 JVM 的关闭。 在这种情况下就需要使用守护线程( Daemon Thread )。 线程可分为两种:普通线程和守护线程。在 JVM 启动时创建的所有线程中, 除了主线程以外,其他的线程都是守护线程(例如垃圾回收器以及其他执行辅助工作的线程)。 当创建一个新线程时,新线程将继承创建它的线程的守护状态,因此在默认情况下,主线程创建的所有线程都是普通线程。 普通线程与守护线程之间的差异仅在于当线程退出时发生的操作。当一个线程退出时, JVM 会检查其他正在运行的线程,如果这些线程都是守护线程,那么 JVM 会正常退出操作。 当 JVM 停止时,所有仍然存在的守护线程都将被抛弃, 且不会执行 finally 代码块,JVM 将直接退出。 我们应尽可能少地使用守护线程 ― 很少有操作能够在不进行清理的情况下被安全地抛弃。特别是, 如果在守护线程中执行可能包含 I/O操作的任务, 那么将是一种危险的行为。守护线程最好用于执行“内部”任务, 例如周期性地从内存的缓存中移除逾期的数据。
-
拒绝策略
拒绝策略 ,当线程池满了的时候,使用的拒绝方式,具体见下图中
一张图带你走进ThreadPoolExecutor的世界
下面我们看下jdk中已经为我们定义好的线程池工具类 Executors
,他其实也是各种 new ThreadPoolExecutor(); 只不过细分了一下。
Executors框架提供的几种线程池简介
在介绍前 有必要了解下java中的阻塞队列
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞 的插入和移除方法。
1. 支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。
2. 支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,
消费者是 从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器
Java中的阻塞队列
-
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
ArrayBlockingQueue是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。 默认情况下不保证线程公平的访问队列,所谓公平访问队列是指阻塞的线程, 可以按照阻塞的先后顺序访问队列,即先阻塞线程先访问队列。非公平性是对先等待的线程是非公平的, 当队列可用时,阻塞的线程都可以争夺访问队列的资格,有可能先阻塞的线程最后才访问队列
-
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
LinkedBlockingQueue是一个用链表实现的有界阻塞队列。此队列的默认和最大长度为 Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。
-
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序升序排列。 也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时, 指定构造参数Comparator来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。
-
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。 队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。 只有在延迟期满时才能从队列中提取元素 DelayQueue非常有用,可以将DelayQueue运用在以下应用场景。 1. 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue, 一旦能从DelayQueue中获取元素时,表示缓存有效期到了。 2. 定时任务调度:使用DelayQueue 保存当天将会执行的任务和执行时间,一旦从 DelayQueue中获取到任务就开始执行,比如TimerQueue就是使用DelayQueue实现的。
-
SynchronousQueue:一个不存储元素的阻塞队列。
SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作, 否则不能继续添加元素。 它支持公平访问队列。默认情况下线程采用非公平性策略访问队列 SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费 者线程。 队列本身并不存储任何元素,非常适合传递性场景。 SynchronousQueue的吞吐量高于 LinkedBlockingQueue和ArrayBlockingQueue
-
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。 相对于其他阻塞队列,LinkedTransferQueue 多了 tryTransfer 和 transfer 方法。 transfer: 如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法 时), transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。 如果没有消费者在等 待接收元素,transfer方法会将元素存放在队列的tail节点, 并等到该元素被消费者消费了才返回。 tryTransfer: 用来试探生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。 和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回, 而transfer方法是必须等到消费者消费了才返回。 对于带有时间限制的tryTransfer(E e,long timeout,TimeUnit unit)方法, 试图把生产者传入 的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回, 如果超时还没消费元素,则返回false,如果在超时时间内消费了元素,则返回true。
-
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。所谓双向队列指的是可以从队列的两端插入和移出元素。 双向队列因为多了一个操作队列的入口,在多线程同时入队 时,也就减少了一半的竞争。相比其他的阻塞队列, LinkedBlockingDeque多了addFirst、 addLast、offerFirst、offerLast、peekFirst和peekLast等方法, 以First单词结尾的方法,表示插入、 获取(peek)或移除双端队列的第一个元素。以Last单词结尾的方法, 表示插入、获取或移除双 端队列的最后一个元素。另外,插入方法add等同于addLast,移除方法remove等效于 removeFirst。 但是take方法却等同于takeFirst,使用时还是用带有First 和Last后缀的方法更清楚些。
1. SingleThreadExecutor
- 创建并返回一个 Executor,它使用单个线程执行任务。 (注意,如果这个单线程在关闭之前由于执行失败而终止,且队列中有等待执行的任务,那么将会有一个新线程取代它。)且他会保证任务按顺序执行,任何时间活动的线程都不会超过一个。
注意其队列长度为 Integer.MAX_VALUE
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- 适用于保证顺序,且只希望有有且仅有一个线程执行任务的场景下 (就算是一个线程 也不要去 new Thread(),因为SingleThreadExecutor不会频繁new 或者销毁线程资源,相反new Thread会这么做)
2. CachedThreadPool
- 线程池数量最大值是 Integer.MAX_VALUE, 且使用不存储任务的队列,即SynchronousQueue (该队列不存储任何东西,换句话说他是容量为0,专门用来两个线程之间传递内容的Queue,他有个特点就是 必须有线程正在执行poll的时候,offer才会成功,否则就失败了),线程的销毁时间为无任务60秒后,且来一个任务就创建一个线程去执行,来一个就执行 ...... ,极端情况下,如果提交任务的速度高于线程处理的速度时候,可能发生耗尽CPU和内存的风险。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- 适用于短期的,异步的轻量级程序,或者负载较轻的服务器。当然我个人觉得这个线程池很少用。用他时一定要考虑好。
3. FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
其 corePoolSize 和 maximumPoolSize 都会设置为调用者传入的nThreads 且 keepAliveTime = 0 ,即意味着,非核心线程池中,如果某个线程没有任务待执行,那么将立即销毁。同样,其使用的队列长度为 Integer.MAX_VALUE
4. ScheduledThreadPoolExecutor
- DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
ok介绍了上边的Executors 但是在实际场景中,我一般倾向于自定义 线程池,不使用 Executors的,原因如下
最后: 如何定义一个好的线程池???
- 我个人的一些观点
-
使用Runtime.getRuntime().availableProcessors()确定你的cpu核数来作为你的corePoolSize,
-
任务队列呢最好是指定个长度,如使用ArrayBlockingQueue并指定长度,
-
自定义拒绝策略,我们如果觉得jdk提供的拒绝策略不够用,那我们可以实现 RejectedExecutionHandler来自定义我们的策略,比如,记录日志到es或者mysql,或者打印个日志等等(怎么处理就看业务场景啦),
-
合理设置keepAliveTime参数,当然这个我在这里也不好说怎么个合理法,总之根据你的业务场景来。
-
监控线程池的状态,这个比较简单,直接调用ThreadPoolExecutor的方法就可以了(但是 实际使用时候,其实我们还可以继承 ThreadPoolExecutor ,然后自定义一些我们想要的东西,我的项目中目前有这些子类) 看下图:
这里我们看下spring是怎么扩展的,我们来看
package org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
这个类到这里我相信你已经知道怎么监控线程池了。 这里贴出监控线程池的几个方法
taskCount:线程池需要执行的任务数量。 completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。 largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过 getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销 毁,所以这个大小只增不减。 getActiveCount:获取活动的线程数。 通过扩展线程池进行监控。可以通过继承线程池来自定义线程池, 重写线程池的 beforeExecute、afterExecute和terminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。 例如,监控任务的平均执行时间、最大执行时间和最小执行时间等
ok关于线程池我们暂时聊到这里,有时间我会将队列的使用示例,以及线程池的源码分析拿出来写两篇文章,以作备忘和分享。
参考: 《Java并发编程的艺术》