0x01 关于ThreadPoolExecutor
上一篇博客已经说了,Executors就是一个工具类。他创建线程池时,实际上是通过如下:
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);
- corePoolSize:线程池中持久保持的核心线程数,可通过setCorePoolSize函数动态更改。
- runnableTaskQueue:用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue
- maximumPoolSize:线程池允许创建的最大线程数,可通过setMaximumPoolSize函数动态更改
- ThreadFactory:用于设置创建线程的工厂。
- RejectedExecutionHandler:当队列和线程池都满了,此时无法加入新的任务,线程池饱和。该参数表示饱和策略,默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略:AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy
- keepAliveTime:默认情况下是指非核心线程变成空闲状态后的存活时间,不影响核心线程,可通过setKeepAliveTime函数动态更改。++但是可通过调用allowCoreThreadTimeOut函数,来设置该变量对核心线程起作用。++
- TimeUnit:keepAliveTime的时间单位。
参考:方腾飞
0x02 ThreadPoolExecutor的工作流程
- 如果线程池中的线程数小于corePoolSize,且无空闲线程,则创建新线程执行任务,若存在空闲线程,且是alive,则复用该线程。
- 如果线程池中的线程数等于corePoolSize,且无空闲线程,且队列未满,那么任务将添加到阻塞队列中,等待执行
- 如果线程池中的线程数等于corePoolSize,且无空闲线程,且队列已满,如果此时线程数小于maximumPoolSize,则会创建新线程执行任务。
- 如果线程池中的线程数等于corePoolSize,且无空闲线程,且队列已满,如果线程数已经大于maximumPoolSize,则根据相应的拒绝策略来处理该任务
- 如果keepAliveTime大于0,默认情况下非核心线程执行玩当前任务时,并不会立即被回收,而是会等待指定的时间,若这段时间中再无任务需要执行则被回收,否则执行该任务。
总结下,更简单直白的说:
- 优先使用corePoolSize范围内的线程
- 其次是阻塞队列。(如果运行线程数大于corePoolSize,则添加到队列中)
- 最后是maximumPoolSize范围内的线程。(如果队列也满了且运行线程数小于maximumPoolSize,则新建线程)
0x03 Executor中提供的创建线程池的五种方法
1.newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
如果nThreads=3,则表示,核心线程数和最大线程数都为3,使用该方法创建的线程池阻塞队列的长度为Integer.MAX_VALUE。此时无论加入的任务有多少,线程池将一直只提供3个线程,并且这三个不论空闲与否都将一直保持在线程池中不被销毁。FixedThreadPool线程池适合用于任务密集型且任务量不大。但需要注意的是如果提供的nThreads数过小,有可能导致阻塞队列满,导致任务被Reject
2.newSingleThreadExecutor
使用该方法创建的线程池,newSingleThreadExecutor内部是使用FinalizableDelegatedExecutorService来创建线程池的,FinalizableDelegatedExecutorService继承自DelegatedExecutorService,这个类是个包装类,将一个ThreadPoolExecutor包装后之暴露出ExecutorService中的接口方法出来,因此newSingleThreadExecutor返回的ExecutorService与newFixedThreadPool(1)是有所不同的,它能够保证ExecutorService不被再次配置。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
3.newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
使用该方法创建的线程池,表示核心线程数为0,最大线程数为Integer.MAX_VALUE,所以这个线程池中的线程变为空闲状态后,只会被保留60秒,然后就会被回收。SynchronousQueue是不能存储元素的阻塞队列,每一个put操作必须等待一个take操作,否则不能继续添加元素,因此,在向这种线程池提交任务时,如果有空闲线程还在keepAlive,那么直接使用该线程,否则将创建新的线程。该线程池适合用于任务简单耗时短的情景。
4.newScheduledThreadPool
该方法与其他方法稍有不同,其内部直接调用ScheduledThreadPoolExecutor的构造方法,ScheduledThreadPoolExecutor是ThreadPoolExecutor的子类,其内部实现了定时执行的逻辑。跟newFixedThreadPool挺类似的,只不过这个线程池使用的是DelayedWorkQueue,DelayedWorkQueue是一个优先级队列,基于堆结构,队列的最前面始终是执行时间最靠前的。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
//ScheduledThreadPoolExecutor是ThreadPoolExecutor的子类
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
5.newWorkStealingPool
这个方法是JDK8中新增加的方法,该方法的内部其实是创建了一个ForkJoinPool(该线程池是搭配着ForkJoinTask来使用的)。何谓ForkJoin呢?其实就是分治、合并,把大的任务划分成多个小的子任务然后分别运行最终把结果汇总合并。那何谓WorkStealing呢?这是一种调度策略,其中已完成自己任务的工作线程可以从其他线程窃取挂起的任务,而并不是空闲。该线程池是并行执行,默认并行量是处理器核心数,任务会在多个处理器之间划分。篇幅问题,就不展开了,下次单独开篇写这个
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
0x03自定义线程池
Part1
阿里的Java开发手册上面是不建议使用Executors来创建线程池的,而是建议自定义创建
Part2
自定义创建还是有很多问题需要注意的
比如:
//阻塞队列的长度应该根据业务需求做出合理估计。
new ThreadPoolExecutor(5,5,0,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>(1000));
如果这样创建一个线程池是很危险的,很有可能会阻塞队列满,导致添加任务被拒绝。因此要考虑到阻塞队列满的时候的处理对策,可以自己设置RejectedExecutionHandler来处理该问题。
并且如果当corePoolSize=maximumPoolSize时,这个时候指定keepAliveTime参数是没有意义的。这些线程是会被持久保持的
可以将上面改为如下,能够减少阻塞队列满的情况:
new ThreadPoolExecutor(5,50,0,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>(1000));
//最好是自己再定义RejectExceptionHandler
new ThreadPoolExecutor(5,50,0,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>(1000),handler);
这样当核心线程都在工作,并且阻塞队列也满了,这个时候就会创建新的线程来执行队列中的任务。
Part3
下面这样的构造方式也是存在问题的:
new ThreadPoolExecutor(5,50,0,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>());
这种构造方式会导致maximumPoolSize基本失效,线程池线程的数量一直是5。
Part4
下面这样的构造方式也是存在问题的:
new ThreadPoolExecutor(0, 50, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
这种构造方式会导致corePoolSize和maximumPoolSize基本失效,新添加的任务将直接被加到阻塞队列中,直到队列满了,才会创建新线程,但是当队列满了的时候,可能已经OutOfMemoryException了。也就是你新添加的任务可能根本不会执行。
Part5
还有这种:
new ThreadPoolExecutor(5, 100, 60, TimeUnit.SECONDS, new SynchronousQueue<>());
如果对任务的数量,执行时间没有明确的认识,这样创建线程池也是很有可能导致Reject。
比如执行如下一段代码:
ExecutorService executorService =
new ThreadPoolExecutor(5, 100, 0, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
for (long i = 0; i <30; i++) {
executorService.execute(() -> {
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(i);
}
由于每个任务执行时间都过长,这将导致线程池数量立马达到最大值,当添加到第101个任务时,将会抛出异常:
rejected from java.util.concurrent.ThreadPoolExecutor@43556938[Running, pool size = 100, active threads = 100, queued tasks = 0, completed tasks = 0]
总结:
自定义线程池时,应该合理根据业务需求估计任务数量,执行速度,来合理的配置线程池的参数。并且最好自定义RejectedExecutionHandler
参考:
https://www.cnblogs.com/vhua/p/5297587.html
https://juejin.im/entry/5afe36a46fb9a07aa213965b