1 简述
线程池主要的功能是对线程的复用、并发数量进行管理,Executors类在线程池构建方面,起到门面工厂作用,提供了ForkJoinPool、ThreadPoolExecutor、ScheduledThreadPoolExecutor线程池类的多个静态构建方法,这些类都实现了ExecutorService接口,对外提供统一的线程管理、任务操作。
ForkJoinPool类,是jdk1.7后提供的线程池管理类,采用多个队列对任务进行管理,池中每个线程都有各自的队列,避免了线程间的竞争,它的控制参数比较简单,主要是parallelism,用它来控制并发级别,也可以理解为池中所允许的最大线程数。
ThreadPoolExecutor、ScheduledThreadPoolExecutor两个线程池类,它们池中的所有线程都共享一个队列。 其中ScheduledThreadPoolExecutor类是对ThreadPoolExecutor类的扩展,二者比较相似。
本文主要介绍常见的ThreadPoolExecutor线程池类。
2 参数
2.1 基本参数
ThreadPoolExecutor类,构造方法包含7个参数:
int corePoolSize:常驻核心线程数。
int maximumPoolSize:最大线程数。
long keepAliveTime:空闲时间,当线程空闲达到该值,且数量大于corePoolSize时,会被销毁。
TimeUnit unit:keepAliveTime的时间单位。
BlockingQueue<Runnable> workQueue:任务队列
ThreadFactory threadFactory:线程工厂
RejectedExecutionHandler handler:拒绝策略
2.1 阻塞队列
队列 | 描述 |
ArrayBlockingQueue | 基于数组实现的有界、阻塞队列。 |
LinkedBlockingQueue | 基于链表实现的有界、阻塞队列。 |
PriorityBlockingQueue | 支持优先级排序的无界、阻塞队列。 |
DelayQueue | 基于优先级队列实现的无界、阻塞队列。 |
SynchronousQueue | 一个无容量的阻塞队列。 |
LinkedTransferQueue | 基于链表实现的无界、阻塞队列。 |
LinkedBlockingDeque | 基于链表结构实现的双向有界、阻塞队列。 |
以上是JDK封装的7个阻塞队列,选择不同的阻塞队列,对线程池的影响很大,在"6 常见问题"部分会详细说明。
2.3 线程工厂
虽然JDK已提供默认的线程工厂Executors.defaultThreadFactory(),如果所有线程池都采用,就无法通过线程名称,便捷判断出哪个业务产生的线程,最好根据实际业务,规范线程命名,自定义实现ThreadFactory接口。
2.4 拒绝策略
JDK提供了4种拒绝策略,由ThreadPoolExecutor类通过公共内部静态类实现,如下:
AbortPolicy 默认策略,在任务饱和时,会抛出RejectedExecutionException异常。
DiscardPolicy 该策略直接抛弃任务,也不抛出异常,通常这是不推荐的作法。
CallerRunsPolicy 在任务饱和时,主线程自己去执行该任务(在并行任务场景,通常可以作为一种理想的托底方案)。
DiscardOldestPolicy 将最早进入队列的任务删掉腾出空间,重新提交执行。
这4个类的源码逻辑,都很简单,如果有个性化业务逻辑需求,也可以自定义实现。
3 线程创建
ThreadPoolExecutor线程池,新增加一个线程处理流程:
补充说明
1. 创建线程池后,在没有处理任务之前,线程池此时还没有线程。
2. 当接到新任务后, 首先判断核心线程数是否达到corePoolSize,如果没有,会创建一个新线程(就算当前有空闲线程,也会创建新线程,且仅创建1个).
3. 如果常驻核心线程已满,任务会尝试进入队列。
4. 如果任务队列也已满, 会尝试创建新线程执行任务。
5. 如果线程池已达到最大线程数, 即线程池处于饱和状态,则按"拒绝策略"处理当前提交的任务。
4 线程销毁
场景1:过期销毁。
如果线程池中空闲线程数大于corePoolSize, 且线程的空闲时间超过keepAliveTime阈值, 该线程就会被销毁。
场景2:主动销毁:shutdownNow()方法。
如果没有正在执行的任务,线程池会直接销毁所有线程;
如果线程池还有正在执行的任务(非队列中待执行任务),会对线程进行"中断处理"。
提示:中断逻辑,不能保证当前正在执行的任务立刻终止,只有当前线程处在可中断情况下,例如:wait, join, sleep等阻塞方法,这些方法会触发中断异常如InterruptedException等,否则需要等任务执行结束, 线程才会被销毁。
场景3:主动销毁:shutdown()方法。
跟shutdownNow()方法不同,不会对正在执行的任务,做"中断处理",它只是把当前线程池设置为关闭状态,此时isShutdwon(), isTerminating()为true, 不允许再向线程池提交新任务。队列中已存在的待处理任务,都将会被执行,等所有任务都执行完毕,线程池才会终止,并销毁所有线程。
5 线程池构建
5.1 Executors类4种静态方法
Executors.newCachedThreadPool
对线程数量没有上限,配置SynchronousQueue无容量阻塞队列。
Executors.newScheduledThreadPool
需配置核心线程数corePoolSize, 尽管没有对线程数设置上限, 但它形同虚设不会起作用,因配置内置的DelayedWorkQueue无限阻塞队列,它的线程数永远不会超过corePoolSize,需留意这个隐含现象。
Executors.newSingleThreadExecutor
创建一个单线程的线程池,配置LinkedBlockingQueue阻塞队列(最大容量Integer.MAX_VALUE)
Executors.newFixedThreadPool
创建一个固定线程数的线程池,配置LinkedBlockingQueue阻塞队列(最大容量Integer.MAX_VALUE)
可以看到,Executors类提供的默认线程池配置参数,通常都不够友好,不是线程无限大,就是队列无限大,很容易使系统失控。
5.2 ThreadPoolExecutor类构造函数
这目前推荐的创建线程池方式,可自定义配置线程数量,阻塞队列、拒绝策略。
例如1: 无容量队列
new ThreadPoolExecutor(5, 100, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
例如2: 有容量队列
new ThreadPoolExecutor(5, 20, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(1000));
等等。
6 线程异常
执行线程任务时,抛出的异常,必须进行捕获处理,避免抛出异常被吞掉,系统却没有任何感知的糟糕情况。线程异常捕获,可通过以下两种方式处理:
第一种: 统一配置默认异常捕获
Thread.setDefaultUncaughtExceptionHandler((t, e)-> {
System.out.println("默认捕获-报错线程:" + t.getName());
System.out.println("默认捕获-线程抛出的异常:" + e);
});
第二种: 线程实例单独配置
该方式,通常会将UncaughtExceptionHandler封装到线程工厂类,如下:
static class MyThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
private final UncaughtExceptionHandler uncaughtExceptionHandler;
MyThreadFactory(String name, UncaughtExceptionHandler uncaughtExceptionHandler) {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = String.format("pool-%s-%d-thread-", name, poolNumber.getAndIncrement());
this.uncaughtExceptionHandler = uncaughtExceptionHandler;
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
if (this.uncaughtExceptionHandler != null) {
t.setUncaughtExceptionHandler(uncaughtExceptionHandler);
}
return t;
}
}
注意:以上两种捕获方式,仅适用于通过ExecutorService的execute方法提交的线程任务,如果通过submit方法提交线程任务,这两种方法就不会起作用,必须通过Future.get方法,在宿主线程进行捕获处理。否则异常就会被吞掉。
7 常见问题
7.1 队列容量对线程数的影响
例1:队列容量超大, 这种情况,队列几乎不会被填满(内存早崩了),导致线程数量达到核心线程数后,就一直维持核心线程数不变,例如下面配置,线程数不会超过5个,最大线程数形同虚设。
new ThreadPoolExecutor(5, 20, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
例2:设置队列1000,这种情况,只有在队列填满1000后,才会创建5个以上的线程。
new ThreadPoolExecutor(5, 20, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(1000));
7.2 无容量队列SynchronousQueue
因该队列无容量,线程的创建、销毁比较灵敏,业务的处理会比较及时,不存在maximumPoolSize形同虚设情况,任务也不易丢失。尽管它没有任务缓冲、负载能力有限,但它可以很好的满足一些实时、并行业务场景。
7.3 队列容量对业务的影响
因任务队列有缓存,任务丢失的风险就不可避免,例如宕机、系统上、下线等等, 就需要考虑业务是否容忍这种风险,以及采取必要的补救措施。
8 总结
1. 需要根据业务场景,谨慎选择阻塞队列。
2. 对拒绝策略的完善处理,需要对拒绝异常捕获处理,避免有逻辑漏洞。
3. 对于自己创建的线程池,需要在进程退出时,主动关闭线程池。
4. 最后,对线程定义一个有意义的名称, 方便对问题排查、分析。