一,前言
大家平时在工作会经常用到线程池进行多线程程序开发,正常做法,新建的线程直接丢到线程池里执行,然后就什么都不管了,一般情况下这样做也没什么错,但是在项目实战中我们吃了太多一般情况的亏,如果线程任务执行的业务逻辑比较耗时,又比如如果系统进行大促销,流量比较大的话,那么大概率(或者说基本)系统会因为线程的积压而导致内存被打爆,资源被吃光。那么究竟该怎样正确使用java的线程池?下面我们一起来讨论这个问题。
二,ExecutorService接口
JDK1.5开始,新增了ExecutorService接口,官方推荐使用这个接口进行线程池的创建。
2.1,Single线程池
//创建单个核心线程数线程池
ExecutorService singlePool = Executors.newSingleThreadExecutor();
具体实现代码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
corePoolSize和maximumPoolSize都设置的为1,但是阻塞队列LinkedBlockingQueue是无界的,这是内存被打爆的隐患,如果流量大,业务线程不断的被塞入,排队队列不断增加最终会导致内存资源耗尽。
2.2,fixed线程池
//创建固定数量线程数线程池
ExecutorService fixPool = Executors.newFixedThreadPool(50);
具体实现代码如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
fixed线程池线程就是N个线程数的Single线程池,和Single线程池有同样的隐患问题,不再赘述。
2.3,cache线程池
//创建无界线程池
ExecutorService cachePool = Executors.newCachedThreadPool();
具体实现代码如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
cache线程池的隐患就更大了,maximumPoolSize为 Integer.MAX_VALUE,可以理解为无限大,意味着可以不断塞入线程,并且排队队列也是没有限制,流量洪峰时内存被打爆风险更大。
2.4,线程池核心参数理解
以上三种方式创建线程,我们进入源码看到它都是用ThreadPoolExecutor类进行线程池的创建,我们看ThreadPoolExecutor类的构造方法,它接收以下六个参数,这就是构建线程池的核心参数
2.4.1,corePoolSize(核心线程数)
核心线程会一直存在,即使没有任务执行。当业务线程数小于核心线程数的时候,即使有空闲线程,也会一直创建线程直到达到核心线程数。设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭。
2.4.2,maximumPoolSize(最大线程数)
线程池里允许存在运行的最大业务线程数量。它和corePoolSize的实际意义,就比如一个篮球馆,正常情况可以容纳5000人看球,corePoolSize就是5000,当遇到热门比赛球馆临时加座一共涌进了8000个球迷看球,maximumPoolSize就是8000。
2.4.3,keepAliveTime(线程空闲时间)
当线程空闲时间达到keepAliveTime时,线程会退出(关闭),直到线程数等于核心线程数,如果设置了allowCoreThreadTimeout=true,则线程会退出直到线程数等于零。
2.4.4,TimeUnit(线程空闲时间的单位)
2.4.5,workQueue(任务队列容量)
也叫阻塞队列,当核心线程都在运行,此时再有任务进来,会进入任务队列,排队等待线程执行。
2.4.6,RejectedExecutionHandler(任务拒绝处理器)
当线程数量达到最大线程数,且任务队列已满时,会执行拒绝处理器。
三,四种线程池的拒绝策略
在使用线程池设定拒绝策略极端重要,一定要考虑线程池满了之后的处理逻辑,错误的使用了拒绝策略会造成业务逻辑上的漏洞。ThreadPoolExecutor类提供了四种内置策略:
3.1,AbortPolicy策略
丢弃任务,抛运行时异常
3.2,CallerRunsPolicy策略
执行任务
3.3,DiscardPolicy策略
忽视,什么都不会发生
3.4,DiscardOldestPolicy策略
从队列中踢出最先进入队列(最后一个执行)的任务
3.5,自定义
当然JDK也提供了自定义的拒绝策略,只要实现RejectedExecutionHandler接口。
四,正确的实践方法:
我建议大家这样使用线程池:
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 20, 10,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(15), new LogRejectedExecutionHandler());
各个线程池的核心参数都自己设定,并且一定要自定义线程池拒绝策略,根据自己的业务逻辑编写拒绝逻辑,不然如果使用内置的拒绝策略,很容易造成业务逻辑上的漏洞。至于各个核心参数设定多少值,可根据实际机器的性能,cpu性能,cpu核心数等等因素,总之使用线程池,不断的调试观察机器性能变化,设置最合理的参数值和拒绝策略。
五,总结:
没有一劳永逸的解决方案,线程池顾名思义它是一个池子,要合理控制进入和流出的线程数量,并且设置合理拒绝策略。
好比篮球馆,它的吞吐量终归有上限的,要控制进入和走出球馆的人流量,进入的量大于出的量球馆就会人满为患,进入的量小于出的量也会造成球馆资源的浪费。当球馆人满为患时,要控制人员进入球馆的流量,并且要跟球迷解释原因,即拒绝策略。好了,啰嗦了这么多希望对大家有用,欢迎留言一起讨论。