上一篇OOM分析之问题定位(一)中讲到经过单例模式能够有效的减小内存使用。可是随着压测并发数的不断提升,QRCodeTask对象不断增长,内存占用相应也会一直增长。再加上QRCodeTask任务的业务功能是合成图片,属于CPU密集型任务。若是处理的QRCodeTask任务太多,会一直占用CPU,形成其它接口响应的速度变慢。html
所以能够对ThreadPoolExecutor深刻研究来找到进一步优化的措施。java
Java SE API文档连接以下:程序员
docs.oracle.com/javase/7/do…api
经过查看源码能够看到全部不一样形参的构造函数最终都会调用到如下的构造函数。数组
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
复制代码
corePoolSize:线程池中核心线程数目,即便空闲也不回收,除非设置了allowCoreThreadTimeOut时间。当有新增任务且当前线程数小于corePoolSize时,会继续建立核心线程执行任务。当线程数达到corePoolSize时,后面新增任务都会加入到BlockingQueue队列中等待执行。缓存
maximumPoolSize:线程池中永许达到的最大线程数母。若是BlockingQueue队列满,且当前线程数小于maximumPoolSize,则线程池会建立新的临时线程继续执行后续任务,直到线程数目达到maximumPoolSize。若是BlockingQueue使用了无界队列,此参数设置了也没有实际意义。bash
keepAliveTime:临时线程空闲后的存活时间,超时后空闲线程会被销毁。并发
unit:keepAliveTime的时间单元。单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS)和毫微秒(NANOSECONDS)。oracle
workQueue:暂存任务的工做队列,执行execute方法提交的Runnable任务都会加入到此队列中。与ThreadPoolExecutor相关的BlockingQueue接口实现类有如下4种:函数
ArrayBlockingQueue:数组实现的有界队列。
LinkedBlockingQueue:链表实现的无界队列,未指明容量时,默认大小为Integer.MAX_VALUE,即21亿多。
SynchronousQueue:不能存储元素的同步队列,主要用于生产者消费者之间的同步。
DelayedWorkQueue:延迟队列,一个无界阻塞队列,只有延迟时间结束才能出队。
ThreadFactory:线程工厂,能够给新建线程设置优先级、名字、是否守护线程、线程组等信息。
RejectedExecutionHandler:任务队列满且线程数也到达极限时的回调函数,用来对没法处理的任务实施拒绝策略,默认策略是AbortPolicy。Executor提供的4种拒绝策略以下:
ThreadPoolExecutor.AbortPolicy:默认策略,抛出java.util.concurrent.RejectedExecutionException异常 。
ThreadPoolExecutor.CallerRunsPolicy: 调用execute()方法 ,重试添加当前的任务。
ThreadPoolExecutor.DiscardPolicy:直接抛弃没法处理的任务。
ThreadPoolExecutor.DiscardOldestPolicy:抛弃队列中最先加进去的任务,即队列头的任务,而后执行execute()方法,从新添加新提交的任务。
除以上4种策略外,ThreadPoolExecutor还能够自定义饱和策略。用户能够灵活的根据实际应用场景实现RejectedExecutionHandler接口,添加符合自身要求的业务代码,如记录日志或持久化不能处理的任务等。
当有新增任务且当前线程数小于corePoolSize时,建立核心线程执行任务。
当线程数达到corePoolSize时,后续新增任务都会加到BlockingQueue队列中排队等待执行。
当BlockingQueue也满后,会建立临时线程执行任务。若是临时线程超过空闲时间后会被销毁。
当线程总数达到maximumPoolSize时,后续新增任务都会被RejectedExecutionHandler拒绝。
Java SE API文档连接以下:
为了方便程序员的使用,Java设计者贴心的在Executors类中实现了4种获取线程池的静态方法,目的是让程序员不关心各个参数的细节就能获得合适本身的线程池。下面是对各个函数进行介绍:
固定大小线程池,无界队列。
构造方法:
public static ExecutorService newFixedThreadPool(intnThreads){
return newThreadPoolExecutor(nThreads,
nThreads,
0L,
TimeUnit.MILLISECONDS,
newLinkedBlockingQueue());
}
public ThreadPoolExecutor(int corePoolSize,
intmaximumPoolSize, longkeepAliveTime,
TimeUnit unit, BlockingQueue workQueue){
this(corePoolSize, maximumPoolSize, keepAliveTime,
unit, workQueue,Executors.defaultThreadFactory(),defaultHandler);
}
复制代码
能够看到corePoolsize=maximumPoolSize,超时时间为0,并用了无界任务队列。当任务小于corePoolsize时,会直接建立新的线程执行新增任务。当任务数等于corePoolsize时,新增任务加到无界队列中。此后全部线程即便空闲也不会回收,会一直保持活动状态直到执行shutdown方法。
若是随着任务的增长,任务队列也满,则执行默认的饱和策略,即抛出异常,进程中止。
单线程线程池,无界队列。若是线程意外中止,会新建一个线程代替它去执行后续任务。能够保证任务都是按序执行。
public staticExecutorService newSingleThreadExecutor() {
return newFinalizableDelegatedExecutorService(
new ThreadPoolExecutor(1,
1,
0L,
TimeUnit.MILLISECONDS,
newLinkedBlockingQueue()));
}
复制代码
corePoolsize=maximumPoolSize=1,超时时间为0,无界任务队列。线程池中只有一个核心线程,当有新增任务时加到无界队列中。
可缓存线程池,无界线程池。
public static ExecutorServicenewCachedThreadPool(
ThreadFactory threadFactory) {
return new ThreadPoolExecutor(
0,
Integer.MAX_VALUE,
60L,
TimeUnit.SECONDS,
newSynchronousQueue(),
threadFactory);
}
复制代码
corePoolsize=0,maximumPoolSize=Integer.MAX_VALUE,超时时间为60s,SynchronousQueue任务队列。若处理任务大于当前线程数,且无空闲线程则新建线程执行任务。如有空闲线程,则重复利用空闲线程。当线程空闲时间超时,会对线程进行销毁。
此线程池与其它线程池的最大不一样点是SynchronousQueue队列不能暂存任务,只能经过线程数的无限增长来处理并发任务。
定时任务线程池,使用无界队列。能够定时以及周期性执行任务。
public staticScheduledExecutorService newScheduledThreadPool(
int corePoolSize,
ThreadFactorythreadFactory) {
return newScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) {
super(corePoolSize,
Integer.MAX_VALUE,
0,
NANOSECONDS,
new DelayedWorkQueue(),threadFactory);
}
复制代码
经过上面的介绍,相信你们对ThreadPoolExecutor的工做机制有了深刻的了解,再回到项目中遇到的问题。
查看代码发现项目中的线程池是使用了Executors.newFixedThreadPool,所以当请求持续增长时,QRCodeTask任务就会一直加到无界队列中等待执行,即便经过单例模式使内存占用获得了优化,可是在并发量大的状况下,内存也可能随着队列元素的无限增长最终致使被撑爆。
根据墨菲定律,若是事情有变坏的可能,无论这种可能性有多小,它总会发生。所以为了不之后线上应用发生故障,必须对这部分代码作进一步的优化。
此处咱们不使用 Executors 去获得线程池,而是直接调用ThreadPoolExecutor构造函数,这样能够经过灵活设置参数来构造知足业务需求的线程池,避免资源耗尽。在此项目中修改以下:
ThreadPoolExecutor executor = newThreadPoolExecutor(
10,
15,
60,
TimeUnit.MINUTES,
new LinkedBlockingQueue(10000),
new CustomThreadFactory(),
newCustomRejectedExecutionHandler());
}
复制代码
建立核心线程为10,最大线程20,超时时间60s,任务队列为10000的线程池,而且使用了自定义的ThreadFactory和RejectedExecutionHandler。为方便之后问题的追溯,在自定义ThreadFactory中定义了本身的线程名,并在RejectedExecutionHandle中实现了知足业务需求的处理方法。当请求任务队列达到10000,线程数达到20时,执行给定的拒绝策略。
最后对改造后的程序进行充分测试,应用性能表现平稳,也符合了业务要求,至此这个问题获得了圆满解决。
推荐阅读:
想要了解更多,关注公众号:七分熟pizza