Java基础真痛苦 之 JDK线程池
前言
在程序中合理的运用多线程能充分协调计算机资源(CPU的利用率、IO等),有利于提高系统的吞吐量。在实际的开发中我们也时常运用多线程处理一些问题,如:有较多的计算任务时启动多线程分批并行计算最后汇总计算结果、一些不需要实时计算结果的业务使用线程异步处理等。
那实际我们是使用时自己来手动创建线程吗?不,不是这样的,线程的创建和销毁需要较大的开销,频繁的创建和销毁线程会浪费大量的系统资源。所以一般的,我们通过线程池来管理线程,达到线程资源复用的效果。
开始今天的议题
线程池的核心参数介绍
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
juc下常用线程池大多是通过实例化上述的TreadPoolExecutor
类实现的,所以这个类是jdk线程池的类,本文我们将对它进行介绍。
首先,得从这一堆的构造器参数开始,它们都是些什么鬼呢?
- corePoolSize:池中的常驻线程数,尽管它们是空闲的也不会被销毁,除非设置[allowCoreThreadTimeOut]
参数为true。
maximuPoolSize:线程池内的最大容量数。
keepAliveTime:当线程池内容量大于corePoolSize时,空闲线程消亡前等待新任务的最长时间。
unit:描述keepAliveTime的时间单位
workQueue:存储等待被执行的任务的阻塞队列
treadFactory:自定义的线程工厂
handler:拒绝策略
AbortPolicy:当线程池内线程数量到达最大时,后续提交的任务直接丢弃并以抛出异常
RejectExecutionException
的方式拒绝。CallerRunsPolicy:直接交给提交该任务的进程执行,如果此时线程池关闭,那么该任务会被丢弃。
DiscardOldestPolicy: 丢弃在队列中时间最久的未被执行的任务(有那么丢丢LRU的感觉),然后加新任务加入。
DiscardPolicy:直接悄咪咪的就把当前这个提交的任务给丢弃了。(是不是有点点ZN不负责任的感觉)
以上7个参数得牢牢记住撒,非常滴关键。现在不理解它的小伙伴咱们继续往后看,将它放置到工作原理中你一眼就能明白。
线程池原理
JDK线程池的工作原理,这玩意理解了对工作中用到的时候非常有帮助,一旦不注意很有可能就掉线程池的坑里了。当然,如果有程序媛小姐姐问你这个线程池底层咋工作哒?你也可以和她娓娓道来,说不定还能收获一枚妹子呢?哈哈哈。。。(梦想还是要有的)
![image-20200313232504382 image-20200313232504382](http://q79nx698f.bkt.clouddn.com/image-20200313232504382.png)
图给出来顺着流程瞅瞅是不是心里有点儿感觉了
有感觉就对了,还没的小伙伴继续,感觉该来还是会来,要坚持!!!
相信你已经看图自个儿能明白了,咱再来个现实的例子,加深体会!
这里借一个银行营业厅的场景类比说明下这个流程。
场景主角介绍:需要办理的业务(线程任务)、柜台工作人员(工作线程)、大厅的等待区座椅(阻塞队列)
大厅参数配置:常驻柜台人员2位,最多柜台工作人员4位,等待区座位数15位。
每年的发行的生肖纪念币都相当火,特别是大龄的叔叔阿姨爷爷奶奶们更加是作为收藏和投资一个渠道。那纪念币得上制定营业厅去取哇,今年由于是「西瓜」本命年,鼠年纪念币必须持有。
于是在2020年1月某天,基友小芳早早去了营业厅取币发现人家刚上班,【1号赶紧坐到柜台】开始为他办理业务,后续又一个客户的用户来了【2小姐姐立马坐回柜台】办理业务,后续用户陆陆续续来了都由大堂经理带值等待区就坐等待。西瓜晃晃悠悠的「进门」「取号」「等待区就坐」一气呵成,是不是想到了在B站上一气呵成的操作呢?好在我坐上了第15个位置。望着柜台的两位小姐姐也已经是忙得不行。此时,一位大叔也进来了发现这等待区也没得坐,柜台也是业务繁忙,大堂经理觉得这样下去可能不行了得加加人手。于是乎,【又开发3号4号两个柜台】办理业务,此时等待区也是满座,柜台工作人员也满了,大堂经理不得不在门口开始【拒绝】接下去新来办理的顾客「您好,现在我们的业务繁忙,请您过会再来!」。
增加了工作人员后慢慢的办理压力得到缓解,等待的人数也逐渐变少。当大堂经理发现经常有几个窗口很长时间没人办理业务后开始嘱咐说“再等10分,还没没接到新办理业务的柜台人员可以撤了,留下2个即可。” 时间一分一秒过去,由于在10分钟内3号4号柜台没有办理新业务,【他俩撤了】。由此大厅开始正常节奏营业。
上面述的例子草草的将JDK线程池的原理映射回了生活,希望是能有助于理解和把握这个流程。
小小的开发规约理解
阿里巴巴Java开发规约,为何禁止通过Executors的静态方法去创建公用线程池?
首先来一眼Executors中常用的线程池创建方法
// 创建固定数目的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// 创建单个线程的线程池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
// 创建可缓存60s的线程池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
可以看到 newFixedThreadPool
和newSingleThreadExecutor
两个方法都是通过new ThreadPoolExecutor(...)
创建的线程池。 在构造器传参的位置都是通过直接new LinedBlockingQueue<Runnable>()
的方式。让我们往下看
/**
* Creates a {@code LinkedBlockingQueue} with a capacity of
* {@link Integer#MAX_VALUE}.
*/
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
显然看到这优秀的你已经知道⚠️,Integer.MAX_VALUE
有多大呢?int在Java中长度为4个字节。那么可表示的最大正整数是2^31-1这么大的数。即以这样的方式创建的队列在线程池可以容纳近2^31-1个等待任务,如果某些任务的耗时较长,同时又有很多的业务进入,我想不用等到2^31-1就已经OOM了(OutOfMemoryError)。
同样的newCachedThreadPool
也是以new TreadPoolExecutor(...)
的方式创建,它的问题在于maximumPoolSize
参数传入了Integer.MAX_VALUE
,即在你这段时间大量的任务进入后会不断创建新的线程直到池中线程数达到2^31-1,那这种情况下也大大滴提高了OOM的风险。
解决阿里巴巴规约中线程池问题——手动实例化线程池
/**
* <pre>类名: WatermelonTreadPool</pre>
* <pre>描述: 线程池工厂</pre>
* <pre>日期: 2020/3/15 3:42 下午</pre>
* <pre>作者: watermelon</pre>
*/
public class WatermelonTreadPoolFactory {
/**
* @Description: 创建固定线程数的池
* @author watermelon
* @date 2020/3/15 3:46 下午
* @param nThreads
* @return java.util.concurrent.ExecutorService
*/
public static ExecutorService newFixedThreadPool(int nThreads, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> waitQueue) {
return new ThreadPoolExecutor(nThreads, nThreads, keepAliveTime, unit, waitQueue);
}
}
这里以FixedThreadPool
为例,其实就是将各参数讲给调用者定制。调用者通过预估设置自己所需的参数
小总结
个人的思考:
- 上述情况在线程池资源公用的情况下产生的几率会更加大,若真的预估到不会有这么庞大的量其实也是可以使用。同时也不推荐在一次业务请求内创建池,池化的对象一般都会比较大,频繁的创建和销毁会有较大的开销。可以使用Spring的线程池,同样要注意的是参数的设置根据实际业务量预估,具体情况具体分析。
- 不要为了使用而使用,线程的使用同样是需要代价的,不正确的使用将会导致一些问题:频繁的上下文切换、异常的数据结果。而在多线程环境中出现的一些问题也并不是必重现的,那么我们开发小伙伴的前期对问题的思考就非常重要。
参考资料: