一、线程池的定义
相比 new Thread() 方法创建线程,Java 提供线程池的好处在于:
- 重用存在的线程,减少线程创建、消亡的开销,性能佳。
- 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
- 提供定时执行、定期执行、单线程、并发数控制等功能。
ExecutorService 是最初的线程池接口,ThreadPoolExecutor 类是对线程池的具体实现,它通过构造方法来配置线程池的参数,我们来分析一下它常用的构造函数吧。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
参数解释:
- corePoolSize:核心线程的数量,默认情况下,即使核心线程没有任务在执行它也存在的,我们固定一定数量的核心线程且它一直存活,这样就避免了一般情况下 CPU 创建和销毁线程带来的开销。我们如果将 ThreadPoolExecutor 的 allowCoreThreadTimeOut 属性设置为 true,那么闲置的核心线程就会有超时策略,这个时间由 keepAliveTime 来设定,即 keepAliveTime 时间内如果核心线程没有回应则该线程就会被终止。allowCoreThreadTimeOut 默认为false,核心线程没有超时时间。
- maximumPoolSize:最大线程数,当任务数量超过最大线程数时其它任务可能就会被阻塞。最大线程数 = 核心线程 + 非核心线程。非核心线程只有当核心线程不够用且线程池有空余时才会被创建,执行完任务后非核心线程会被销毁。
- keepAliveTime:非核心线程的超时时长,当执行时间超过这个时间时,非核心线程就会被回收。当 allowCoreThreadTimeOut 设置为 true 时,此属性也作用在核心线程上。
- unit:枚举时间单位,TimeUnit。
- workQueue:任务队列,我们提交给线程池的 runnable 会被存储在这个对象上。
- threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
- handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。
线程池的分配遵循这样的规则:
- 当 currentSize < corePoolSize 时,直接启动一个核心线程并执行任务。
- 当 currentSize >= corePoolSize、并且 workQueue 未满时,添加进来的任务会被安排到 workQueue 中等待执行。
- 当 workQueue 已满,但是 currentSize < maximumPoolSize 时,会立即开启一个非核心线程来执行任务。
- 当 currentSize >= corePoolSize、workQueue 已满、并且 currentSize > maximumPoolSize 时,那么线程池则拒绝执行该任务,且 ThreadPoolExecutor 会调用 RejectedtionHandler 的 rejectedExecution 方法来通知调用者。
二、任务队列
当线程数量大于等于 corePoolSize,workQueue 未满时,则将新任务插入 workQueue。这里要考虑使用什么类型的容器缓存新任务,通过 JDK 文档介绍,我们可知道有 3 种类型的容器可供使用,分别是同步队列,有界队列和无界队列。对于有优先级的任务,这里还可以增加优先级队列。Java 一共为我们提供了7种阻塞队列的实现以:
实现类 | 类型 | 说明 |
---|---|---|
SynchronousQueue | 同步队列 | 该队列不存储元素,每个插入操作必须等待另一个线程调用移除操作,否则插入操作会一直阻塞 |
ArrayBlockingQueue | 有界队列 | 基于数组的阻塞队列,按照 FIFO 原则对元素进行排序 |
LinkedBlockingQueue | 无界队列 | 基于链表的阻塞队列,按照 FIFO 原则对元素进行排序 |
PriorityBlockingQueue | 优先级队列 | 具有优先级的阻塞队列 |
DelayQueue | 类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现Delayed接口,通过执行时延从队列中提取任务,时间没到任务取不出来。 | |
LinkedBlockingDeque | 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样FIFO(先进先出),也可以像栈一样FILO(先进后出) | |
LinkedTransferQueue | 它是ConcurrentLinkedQueue、LinkedBlockingQueue和SynchronousQueue的结合体,但是把它用在ThreadPoolExecutor中,和LinkedBlockingQueue行为一致,但是是无界的阻塞队列。 |
三、线程工厂
线程工厂指定创建线程的方式,需要实现 ThreadFactory 接口,并实现 newThread(Runnable r) 方法。该参数可以不用指定,Executors 框架已经为我们实现了一个默认的线程工厂:
private static class DefaultThreadFactory 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;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
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);
return t;
}
}
四、拒绝策略
线程数量大于等于 maximumPoolSize 且 workQueue 已满,则使用拒绝策略处理新任务。Java 线程池提供了 4 种拒绝策略实现类,如下:
实现类 | 说明 |
---|---|
AbortPolicy | 丢弃新任务,并抛出 RejectedExecutionException |
DiscardPolicy | 不做任何操作,直接丢弃新任务 |
DiscardOldestPolicy | 丢弃队列队首的元素,并执行新任务 |
CallerRunsPolicy | 由调用线程执行新任务 |
以上 4 个拒绝策略中,AbortPolicy 是线程池实现类所使用的策略。我们也可以通过下面这个方法修改线程池决绝策略:
setRejectedExecutionHandler(RejectedExecutionHandler)
五、线程池的分类
Java 中为我们提供了四类具有不同特性的线程池分别为 FixThreadPool、CachedThreadPool、ScheduleThreadPool 以及 SingleThreadExecutor。当然,我们也可以通过 ThreadPoolExecutor 自定义线程池。
5.1、FixThreadPool
public static ExecutorService newFixThreadPool(int nThreads){
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
//使用
Executors.newFixThreadPool(5).execute(r);
从配置参数来看,FixThreadPool 只有核心线程,并且数量固定的,也不会被回收。使用 LinkedBlockingQueue 无界队列,所有线程都活动时,因为队列没有限制大小,新任务会等待执行。由于线程不会回收,FixThreadPool会更快地响应外界请求。
我们可以这么想,FixThreadPool 其实就像一堆人排队上公厕一样,可以无数多人排队,但是厕所位置就那么多,而且没人上时,厕所也不会被拆迁。由于线程不会回收,FixThreadPool 会更快地响应外界请求,这也很容易理解,就好像有人突然想上厕所,公厕不是现用现建的。
5.2、SingleThreadPool
public static ExecutorService newSingleThreadPool (){
return new FinalizableDelegatedExecutorService ( new ThreadPoolExecutor (1, 1, 0, TimeUnit. MILLISECONDS,
new LinkedBlockingQueue<Runnable>()) );
}
//使用
Executors.newSingleThreadPool ().execute(r);
从配置参数可以看出,SingleThreadPool 只有一个核心线程,确保所有任务都在同一线程中按顺序完成。因此不需要处理线程同步的问题。跟 FixThreadPool 一样,它也是使用 LinkedBlockingQueue 无界队列。它的意义在于,统一所有的外界任务到同一线程中,让调用者可以忽略线程同步问题。
可以把 SingleThreadPool 简单的理解为 FixThreadPool 的参数被手动设置为 1 的情况,即Executors.newFixThreadPool(1).execute(r)。所以 SingleThreadPool 可以理解为公厕里只有一个坑位,先来先上。
5.3、CachedThreadPool
public static ExecutorService newCachedThreadPool(int nThreads){
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit. SECONDS,
new SynchronousQueue<Runnable>());
}
//使用
Executors.newCachedThreadPool().execute(r);
CachedThreadPool 只有非核心线程,最大线程数非常大,所有线程都活动时,会为新任务创建新线程,否则利用空闲线程(60s空闲时间,过了就会被回收,所以线程池中有0个线程的可能)处理任务。采用 SynchronousQueue 同步队列相当于一个空集合,导致任何任务都会被立即执行。比较适合执行大量的耗时较少的任务。
可以这么想,CachedThreadPool 就像是一堆人去一个很大的咖啡馆喝咖啡,里面服务员也很多,随时去,随时都可以喝到咖啡。但是为了响应国家的“光盘行动”,一个人喝剩下的咖啡会被保留60秒,供新来的客人使用。如果你运气好,没有剩下的咖啡,你会得到一杯新咖啡。但是以前客人剩下的咖啡超过60秒,就变质了,会被服务员回收掉。
5.4、ScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize){
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize){
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedQueue ());
}
//使用,延迟1秒执行,每隔2秒执行一次Runnable r
Executors.newScheduledThreadPool(5).scheduleAtFixedRate(r, 1000, 2000, TimeUnit.MILLISECONDS);
核心线程数固定,非核心线程(闲着没活干会被立即回收)数没有限制。从上面代码也可以看出,ScheduledThreadPool 主要用于执行定时任务以及有固定周期的重复任务。
通过Executors的newScheduledThreadPool()方法来创建,ScheduledThreadPool线程池像是上两种的合体,它有数量固定的核心线程,且有数量无限多的非核心线程,但是它的非核心线程超时时间是0s,所以非核心线程一旦空闲立马就会被回收。这类线程池适合用于执行定时任务和固定周期的重复任务。
六、关闭线程池
其实无非就是两个方法 shutdown()、shutdownNow()。但他们有着重要的区别:
- shutdown():执行后停止接受新任务,会把队列的任务执行完毕。将线程池状态变为 SHUTDOWN。
- shutdownNow():也是停止接受新任务,但会中断所有的任务,将线程池状态变为 STOP。