一、线程池好处
- 降低资源消耗
通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 - 提高响应速度
当任务到达时,任务可以不需要等到线程创建就能立即执行。 - 提高线程的可管理性
线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源。还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用。
二、线程池组成
一般的线程池主要分为以下 4 个组成部分:
- 线程池管理器:用于创建并管理线程池
- 工作线程:线程池中的线程
- 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
- 任务队列:用于存放待处理的任务,提供一种缓冲机制
三、线程池参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
线程池的构造函数有7个参数,分别是:
- corePoolSize:线程池核心线程大小
- maximumPoolSize:线程池最大线程大小
- keepAliveTime:空闲线程存活时间
- unit:空闲线程存活时间单位
- workQueue:工作队列
- threadFactory:线程工厂
- handler:拒绝策略
1.corePoolSize
核心线程数
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut(允许核心线程超时)。这里的最小线程数量即是corePoolSize。
2.maximumPoolSize
最大线程数
一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接将任务交给这个空闲线程来执行,如果没有则会缓存到工作队列中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。
线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
3.keepAliveTime
空闲线程存活时间
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁(重点:超过核心线程数
、包括核心线程只要是超时的空闲线程都会被销毁),这里的指定时间由keepAliveTime来设定。
4.unit
keepAliveTime的计量单位。
5.workQueue
工作队列
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。
一般的工作队列是先进先出FIFO(First in first out)的。jdk中提供了4种工作队列。
5.1.ArrayBlockingQueue
基于数组的有界阻塞队列
按先进先出排序,有界的数组可以防止资源耗尽问题。
当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
5.2.LinkedBlockingQuene
基于链表的无界阻塞队列
按先进先出排序,其实最大容量为Interger.MAX。
由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
5.3.SynchronousQuene
不缓存任务的阻塞队列
当线程池中线程数量达到corePoolSize后,生产者往队列中放入一个任务必须等到消费者取出这个任务(生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样)。
也就是说如果线程池中线程数量达到corePoolSize,且该队列已放入一个任务还没被消费,新任务进来时,不会放入队列,而是直接创建新线程执行该任务(可以理解为该队列只能存一个任务),如果线程数量达到maxPoolSize,则执行拒绝策略。
5.4.PriorityBlockingQueue
具有优先级的无界阻塞队列
优先级通过参数Comparator实现。
6.threadFactory
线程工厂
创建一个新线程时使用的工厂,可以用来设置线程名、设置线程优先级、设置线程是否为守护线程等。
7.handler
拒绝策略
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略。
7.1.CallerRunsPolicy
由调用线程(提交任务的线程)自己处理该任务。如果调用线程已经关闭,则直接抛弃任务。
7.2.AbortPolicy
直接丢弃任务,并抛出RejectedExecutionException异常。
7.3.DiscardPolicy
直接丢弃任务,什么都不做。
7.4.DiscardOldestPolicy
抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。
四、线程池工作原理
- 线程池中线程数小于corePoolSize时:新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程
- 线程池中的线程数达到corePoolSize时:新提交任务将被放入workQueue中,等待线程池中任务调度执行
- 工作队列满了,且maximumolSize大于corePoolSire时:新提交任务会创建新线程执行任务
- 工作队列满了,且线程池线程数已达到maxmumPoolSize:4.新提交任务直接由RejectedExecutionHandler(拒绝策略)处理
- 线程池中线程数超过corePoolSize:空闲时间超过keepAliveTime的线程会被销毁(重点:
超过核心线程数
、包括核心线程只要是超时的空闲线程都会被销毁)
- 线程池中线程数小于等于corePoolSize,并且还有空闲线程时:workQueue中等待的任务会被执行
- 当设置allowCoreThreadTimeOut为true时:任何线程空闲时间超过kepAliveTime后就会被销毁(重点:
无需超过核心线程数
、包括核心线程只要是超时的空闲线程都会被销毁)
五、任务提交方式
无返回值:execute()
用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功
有返回值:submit()
用于提交需要返回值的任务。
线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值。
get()方法会阻塞当前线程直到任务完成,而使用get(longtimeout,TimeUnitunit),在指定的时间内会等待任务执行,超时则抛出超时异常,等待时候会阻塞当前线程。
六、线程池状态
线程池有五种运行状态
RUNNING
(1)状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
(2)状态切换:线程池的初始化状态是RUNNING。线程池被一旦被创建,就处于RUNNING状态,且线程池中的任务数为0
SHUTDOWN
(1)状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
(2)状态切换:调用线程池的shutdown()接口时,线程池由RUNNING->SHUTDOWN。
STOP
(1)状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
(2)状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNINGorSHUTDOWN)->STOP。
TIDYING
(1)状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
(2)状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由SHUTDOWN->TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP->TIDYING。
TERMINATED
(1)状态说明:线程池彻底终止,就变成TERMINATED状态。
(2)状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由TIDYING->TERMINATED。
七、线程池关闭
ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow()。
它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程。
shutdown()
不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
shutdownNow()
立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。
只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。
八、线程池监控
通过线程池提供的参数可以对线程池的使用请求进行监控,在监控线程池的时候可以使用以下属性,获取线程池任务状况
- taskCount:线程池需要执行的任务数量。
- completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。
- largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是
否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过。 - getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销
毁,所以这个大小只增不减。 - getActiveCount:获取活动的线程数。
九、线程池种类
Java一共提供了4种线程池,通过Executors类的4个静态方法可以获得实例
singleThreadPoolExecutor
单线程池
Executors.newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
特征:
- 核心线程数、最大线程数都为1
- 线程空闲时并不释放
- 基于链表的无界阻塞队列(最大容量Interger.MAX,先进先出)
fixedThreadPoolExecutor
固定线程池
Executors.newFixedThreadPool()
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
特征:
- 可指定核心线程数和最大线程数(核心线程数和最大线程数相同)
- 线程空闲时并不释放
- 基于链表的无界阻塞队列(最大容量Interger.MAX,先进先出)
cachedThreadPoolExecutor
缓存线程池
Executors.newCachedThreadPool()
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
特征:
- 核心线程数0
- 最大线程数Integer.MAX_VALUE
- 空闲线程存活时间60s
- 不缓存任务的阻塞队列(队列只能放一个任务,且取出了才能继续放下一个任务)
scheduleThreadPoolExecutor
延迟线程池
Executors.newScheduledThreadPool()
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
//从ScheduledThreadPoolExecutor的定义可以知道它也是ThreadPoolExecutor的子类
public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor
implements ScheduledExecutorService {
......
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
//super即父类的构造函数,也就是ThreadPoolExecutor()
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
特征:
- 可指定核心线程数
- 最大线程数Integer.MAX_VALUE
- 线程空闲时并不释放
- 延迟工作队列(无界序列,最大值Integer.MAX_VALUE,所以最大线程数是无效的)
对延迟线程池,每次任务提交时,都会先放到工作队列中,这点比较特殊。
它不会像其他线程池:当当前线程数没超过核心线程数是,进来的任务都是直接创建新线程执行。当当前线程数达到核心线程数时,才把任务放到工作队列中。
ScheduledThreadPoolExecutor原理
ScheduledThreadPool线程池中的所有工作线程需要处理的都是计划任务,这个任务分为两种,一种是重复任务,一种是一次性任务。
- 一次性任务:基本上是指定在提交任务多长时间之后开始执行
- 重复性任务:就是在任务被执行完成之后,在一定的时间间隔之后还会被执行,重复任务具体什么时候被执行主要与任务设置的执行时间间隔有关。
而ScheuledThreadPoolExecutor主要是被设计用来维护和调度执行计划任务的线程资源以及任务队列中任务的调整。
提交任务
ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,所以具备线程池提交任务的一般方法submit()与execute()。
同时还具备提交定时任务的几个方法,如下所示:
-
延后delay时长执行Runnable任务
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) { if (command == null || unit == null) throw new NullPointerException(); RunnableScheduledFuture<?> t = decorateTask(command, new ScheduledFutureTask<Void>(command, null, triggerTime(delay, unit))); delayedExecute(t); return t; }
-
延后delay时长执行Callable任务
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) { if (callable == null || unit == null) throw new NullPointerException(); RunnableScheduledFuture<V> t = decorateTask(callable, new ScheduledFutureTask<V>(callable, triggerTime(delay, unit))); delayedExecute(t); return t; }
-
延后执行Runnable 任务,以后每隔period的时长再次执行该任务
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { if (command == null || unit == null) throw new NullPointerException(); if (period <= 0) throw new IllegalArgumentException(); ScheduledFutureTask<Void> sft = new ScheduledFutureTask<Void>(command, null, triggerTime(initialDelay, unit), unit.toNanos(period)); RunnableScheduledFuture<Void> t = decorateTask(command, sft); sft.outerTask = t; delayedExecute(t); return t; }
-
延后执行Runnable 任务,以后任务执行完成后等待delay时长,再次执行任务
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { if (command == null || unit == null) throw new NullPointerException(); if (delay <= 0) throw new IllegalArgumentException(); ScheduledFutureTask<Void> sft = new ScheduledFutureTask<Void>(command, null, triggerTime(initialDelay, unit), unit.toNanos(-delay)); RunnableScheduledFuture<Void> t = decorateTask(command, sft); sft.outerTask = t; delayedExecute(t); return t; }
将提交的任务都会被封装成ScheduledFutureTask类型的任务
ScheduledFutureTask是ScheduledThreadPoolExecutor中的一个内部类,实现了Runnable接口,也就是说,ScheduledFutureTask是一个线程任务。
-
ScheduledFutureTask的关键参数:
- time 任务开始的时间
- period 任务执行的时间间隔
- sequenceNumber 任务的序号
- outerTask 当前任务,此对象的引用会被传入到周期性执行任务的ScheduledThreadPoolExecutor类的reExecutePeriodic方法中。
- heapIndex 进入延迟队列的索引值,它便于取消任务
-
ScheduledFutureTask的构造方法:
ScheduledFutureTask(Runnable r, V result, long ns) { super(r, result); this.time = ns;//任务开始的时间 this.period = 0;//任务执行的时间间隔 this.sequenceNumber = sequencer.getAndIncrement();//任务的序号 }
-
ScheduledFutureTask的排序方法:
public int compareTo(Delayed other) { // 如果与自己比较则返回0 if (other == this) // compare zero if same object return 0; // 放进来的任务为ScheduledFutureTask类型 if (other instanceof ScheduledFutureTask) { ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other; // 当前任务的延迟时间与比较任务的延迟时间之差 long diff = time - x.time; // 如果小于0,则返回-1 if (diff < 0) return -1; // 如果大于0,则返回1 else if (diff > 0) return 1; // 如果等于0,则比较任务的序列号,当前任务序列号小则返回-1 else if (sequenceNumber < x.sequenceNumber) return -1; // 当前序列号大则返回1 else return 1; } // 任务不为ScheduledFutureTask类型,则直接比较两者的延迟时间 long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS); return (diff < 0) ? -1 : (diff > 0) ? 1 : 0; }
排序规则(当前任务和参数任务比较):
- 先根据time(任务开始的时间)排序;
- time相同的再根据sequenceNumber(任务的序号)排序
排序是为了比较两个任务在工作队列中的被线程消费先后顺序。
添加任务到工作队列中
ScheduledThreadPoolExecutor#delayedExecute(t)方法
private void delayedExecute(RunnableScheduledFuture<?> task) {
// 线程池关闭就拒绝任务
if (isShutdown())
reject(task);
else {
// 添加任务到延迟队列中,而ThreadPoolExecute线程池是线程
// 数大于核心线程时才添加任务到阻塞队列,这里是第一次添加
// 任务,因为是定时线程池,在延迟队列中的任务执行run方法时
// 会将任务继续添加进延迟队列
super.getQueue().add(task);
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) &&
remove(task))
task.cancel(false);
else
// 这里是增加一个worker线程,避免提交的任务没有worker去执行
ensurePrestart();
}
}
// 该方法是ThreadPoolExecute线程池里面的方法
void ensurePrestart() {
int wc = workerCountOf(ctl.get());
if (wc < corePoolSize)
//这里就和ThreadPoolExecute线程池里的方法一样,创建一个没有任务的线程
addWorker(null, true);
else if (wc == 0)
addWorker(null, false);
}
十、各种线程池使用场景
- 单线程池适用强调任务必须串行执行的场景,强调次序。
- 定长线程池比较中庸,适用一般场景。
- 缓存线程池适合需要及时处理、处理时间短、数量大的任务,需要规避线程阻塞。
- 延时线程池可以替代定时器,处理定时任务和定时重复任务。