如果大家使用过P3C,在使用Executors创建线程时一定看到过这么一句话:
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors返回的线程池对象的弊端如下:
1)FixedThreadPool和SingleThreadPool:
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2)CachedThreadPool:
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
本文的内容就是对Java线程池相关知识进行比较详细的介绍。
作为Java初学者,我们经常会使用Executors.newXXX
来创建线程池,不建议使用的原因上面阿里爸爸已经说明了,我们就一起看看newXXX
的源码中进行了什么操作吧。
1、newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
2、newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
3、newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
上面三个方法都还有一个ThreadFactory
参数的重载,我们可以看到这三个方法其实就是使用一些默认参数为我们新建了ThreadPoolExecutor
对象,使用起来更为方便快捷。
接下来就一起看看我们自己使用ThreadPoolExecutor
怎么创建线程吧。
下面是ThreadPoolExecutor
的满参构造函数(删掉了参数值的判断和赋值):
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
......
}
1) corePoolSize
核心线程数,指在线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。
2) maxPoolSize
当线程数大于或等于核心线程,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maxPoolSize。如果线程数已等于maxPoolSize,且任务队列已满,则已超出线程池的处理能力,线程池会拒绝处理任务而抛出异常。
3) keepAliveTime
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定。
4) unit
keepAliveTime的计量单位。
5) workQueue
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。
Java中的工作队列可以是BlockingQueue
的四种实现:ArrayBlockingQueue
、LinkedBlockingQuene
、SynchronousQuene
、PriorityBlockingQueue
。
6) threadFactory
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程、ExceptionHandler等等。
7) handler
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:
上面我们已经介绍了ThreadPoolExecutor
构造方法中各个参数的作用,但是各个参数该怎么设置还需要进一步了解。
1) corePoolSize
现在通常是将corePoolSize设置成每秒需要的线程数。
平均每个任务需要花费tasktime
秒来处理,则每个线程每秒可以执行1/tasktime
个任务。系统每秒有tasks
个任务需要处理,则需要的线程数为:tasks/(1/tasktime)
,即tasks * tasktime
个线程数。
假设系统每秒任务数为100 ~ 1000,每个任务耗时0.1秒,则需要100 * 0.1至1000 * 0.1,即10~100个线程。那么corePoolSize应该设置为大于10,具体数字最好根据8020原则,即80%情况下系统每秒任务数,若系统80%的情况下第秒任务数小于200,最多时为1000,则corePoolSize可设置为20。
2) queueCapacity
queueCapacity = (coreSizePool / taskCost) * responseTime
上式中responseTime
表示系统对任务的相应时间。如果采用我们上面的例子,假设相应时间设置为2,则队列长度可以设置为:
(corePoolSize/tasktime) * responsetime: (20/0.1)*2 = 400
队列长度设置过大,会导致任务响应时间过长,切忌以下写法:
LinkedBlockingQueue queue = new LinkedBlockingQueue();
这实际上是将队列长度设置为Integer.MAX_VALUE,将会导致线程数量永远为corePoolSize,再也不会增加,当任务数量陡增时,任务响应时间也将随之陡增。
3) maxPoolSize
maxPoolSize = (max(tasks) - queueCapacity) / (1 / taskCost)
当系统负载达到最大值时,核心线程数已无法按时处理完所有任务,这时就需要增加线程。每秒200个任务需要20个线程,那么当每秒达到1000个任务时,则需要(1000 - queueCapacity) * (20 / 200),即60个线程,可将maxPoolSize设置为60。
线程池按以下行为执行任务