Executor接口:定义execute()
线程池的初衷是屏蔽线程的创建,线程复用等特点,基于这个设计思想,JDK设计了Executor这个顶级接口:
这个接口只有一个抽象方法。
ExecutorService接口:引入线程池状态+Future
Executor顶级接口就一个方法,过于单薄,ExecutorService继承Executor接口,引入了一些新的方法:
ExecutorService比较与父类Executor的特点:
1、引入了线程池的状态:要求子类必须实现诸如shutDown()、shutDownNow()、isTerminated()、isShutDown()等方法以便反馈线程池的状态:已关闭?任务已结束?
2、引入submit()方法,它的灵感来自于Executor的execute(),但execute()是没有返回值的,而submit()会返回一个Future对象(这玩意是FutureTask的爸爸),也就是说,从ExecutorService开始,线程池开始有返回值了。
ScheduledExecutorService接口:定时任务
ScheduledExecutorService在ExecutorService的基础上提出了定时任务池的概念。
ScheduledExecutorService要求子类实现定时方法,能够周期性地执行任务。与submit()类似,当任务提交给这些定时方法后,会返回ScheduledFuture
AbstractExecutorService抽象类
目前为止,我们看到的都是JDK中关于线程池的接口,它们之间的关系是这样的:
这些接口只是规定了一些方法,并没有具体的实现,所以JDK提供了AbstractExecutorService抽象类:
AbstractExecutorService主要做了两件事:
1、新增了newTaskFor()方法,把runnable和callable任务封装成了FutureTask类型
2、对ExecutorService接口中submit()方法进行了实现了
newTaskFor()就是把runnable和callable任务封装成FutureTask类型。
为什么AbstractExecutorService为什么要引入FutureTask呢,因为线程池的顶级接口只能执行Runnable,引入FutureTask,可以兼容callable。
所以,线程池为什么要引入FutureTask包装Runnable和Callable?
● Executor顶级接口的历史原因,需要包装Runnable和Callable,统一返回值问题
● 谋求更强大的功能(异步+结果)
ForkJoinPool&ThreadPoolExecutor
JDK的线程池设计发展到这一步,终于出现了两个划时代的实现类:
主要说一下ThreadPoolExecutor实现类:
ThreadPoolExecutor的贡献有以下几点:
1、 首先引入corePoolSize、maximumPoolSize、keepAliveTime、ThreadFactory、RejectHandler等参数配置,真正实现了线程的“池化”。线程池里的线程数、队列长度、拒绝策略会在这几个阈值的影响下动态调整,大大提高了执行效率和复用率。
2、抽象出workers(线程池),一个Worker就是一个线程
3、抽象出workQueue(阻塞队列),缓冲待执行任务
4、内置4种拒绝策略,规避执行压力
在了解ThreadPoolExecutor之前需要先了解一些其他的东西:
池化技术
比如数据库连接池、常量池、线程池、对象池等等。池化技术是计算机世界里比较常用的、行之有效的优化手段。线程池中的“池”,到底指代什么?
我们向Executor提交任务,Executor自己维护Thread,这个"池"就是Thread集合。
和数据库连接池不同的是:我们通过数据库连接池可以取出一个connection,执行sql语句后,使用close()方法返回connection;但是你不可能从线程池中获取一个线程的,而是把要执行的任务丢到线程池中。
生产消费模型
如果往线程池不断提交任务,大致会经历4个阶段:
● 核心线程处理任务
● 任务进入任务队列等待
● 非核心线程处理任务
● 拒绝策略
特别是第二个阶段,来不及处理的任务会被暂存入workQueue(任务队列),于是典型的生产消费模型就出现了。
几个重要概念
线程池如何复用线程?
先不管如何复用线程,先想一下如何回收/销毁线程?
“线程”这个词,其实有两个层次的指代:Thread对象、JVM线程资源(本质还是操作系统线程)。Thread对象与线程资源之间是绑定关系,一个线程资源被分配后,会找到Thread#run()作为代码的执行入口。
线程什么时候销毁呢?正常来说,new Thread(tartget).start()后,操作系统就会分配线程资源,而等到线程执行完Thread#run()中的代码,就会自然消亡。至于Thread对象,如果没有引用,也会被GC回收。
看到这里你可能就明白了,只要任务永远不结束,线程就永远死不了。任务如何才能永远不结束呀?要么循环做任务、要么阻塞。
线程池本质也是Thread,只是单体和集合的区别。既然Thread“跑完任务就销毁”的特性是天生的、注定的,线程池也无法改变这一点。所以,线程池要想让内部线程一直存活着,就要keeps threads busy working,也就是让它们一直干活。实在没活干怎么办?那就阻塞着呗(可以用阻塞队列)!总之,不能让你“执行完毕”,否则就销毁了。
如何保证只销毁“非核心线程”
简单粗暴:
● 当前线程数 <= corePoolSize,那么所有线程都是核心线程,不回收
● 当前线程数 > corePoolSize,回收多余线程
超过corePoolSize后为什么优先入队?
首先,入队的好处是缓冲执行任务,队列满了之后才扩展线程执行任务;其次,线程资源比较宝贵,用队列缓冲的话,就别用额外创建线程。
简单版TheadPool
public class SimpleThreadPool {
//任务队列,线程超过核心线程数,会把任务放到阻塞队列中
BlockingQueue<Runnable> workQueue;
//工作线程
List<Worker> workers = new ArrayList();
//构造器
SimpleThreadPool(int poolSize,BlockingQueue<Runnable> workQueue){
this.workQueue = workQueue;
//创建线程,并加入到线程池
for (int i = 0; i < poolSize; i++) {
Worker worker = new Worker();
worker.start();
workers.add(worker);
}
}
/**
* 提交任务
*/
public void execute(Runnable task){
// 任务队列满了则阻塞
try {
workQueue.put(task);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 工作线程,负责执行任务
*/
class Worker extends Thread{
@Override
public void run() {
//循环获取任务,如果任务为空则阻塞,线程不会销毁
while (true){
try {
Runnable task = workQueue.take();
task.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
测试
public class SimpleThreadPoolTest {
public static void main(String[] args) {
SimpleThreadPool threadPool = new SimpleThreadPool(2, new ArrayBlockingQueue<Runnable>(2));
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("第一个任务开始");
sleep(3);
System.out.println("第一个任务结束");
}
});
threadPool.execute(()->{
System.out.println("第二个任务开始");
sleep(3);
System.out.println("第二个任务结束");
});
threadPool.execute(()->{
System.out.println("第三个任务开始");
sleep(3);
System.out.println("第三个任务结束");
});
}
private static void sleep(int seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
复杂版ThreadPool
public class ThreadPool {
private final ReentrantLock mainLock = new ReentrantLock();
/**
* 工作线程
*/
private final List<Worker> workers = new ArrayList<>();
/**
* 任务队列
*/
private BlockingQueue<Runnable> workQueue ;
/**
* 核心线程数
*/
private final int corePoolSize;
/**
* 最大线程数
*/
private final int maxPoolsize;
/**
* 非核心线程空闲时间
*/
private final long keepAliveTime;
public ThreadPool(int corePoolSize,
int maxPoolsize,
long keepAliveTime,
TimeUnit timeUnit,
BlockingQueue<Runnable> workQueue){
this.corePoolSize=corePoolSize;
this.maxPoolsize = maxPoolsize;
this.keepAliveTime = timeUnit.toNanos(keepAliveTime);
this.workQueue = workQueue;
}
/**
* 提交任务
*/
public void execute(Runnable task){
//参数为空,抛错
Assert.notNull(task,"task is null");
//创建核心线程,处理任务
if (workers.size()<corePoolSize){
this.addWork(task,true);
return;
}
//尝试加入任务队列
boolean offer = workQueue.offer(task);
if (offer){
return;
}
//任务队列也满了之后,创建非核心线程处理任务
if (!this.addWork(task,false)){
// 非核心线程数达到上限,触发拒绝策略
throw new RuntimeException("拒绝策略");
}
}
public boolean addWork(Runnable task,boolean flag){
int size = workers.size();
//如果当前线程数大于等于核心线程数 或者创建非核心线程时,线程数大于等于最大线程数 返回false
if (size>=(flag?corePoolSize:maxPoolsize)){
return false;
}
boolean workerStarted = false;
try {
Worker worker = new Worker(task);
final Thread thread = worker.getThread();
if (thread!=null){
mainLock.lock();
workers.add(worker);
thread.start();
workerStarted = true;
}
} finally {
mainLock.unlock();
}
return workerStarted;
}
private void runWorker(Worker worker){
Runnable task = worker.getTask();
try {
//循环处理任务
while (task!=null || (task = getTask()) != null){
task.run();
task = null;
}
} finally {
/**
* 从循环中退出来,也就意味着当前线程不是核心线程,需要销毁
*/
workers.remove(worker);
}
}
private Runnable getTask(){
boolean TimeOut = false;
// 循环获取任务
for (;;){
//当前线程数超过核心线程数,返回null
boolean timed = workers.size()>corePoolSize;
if (timed && TimeOut){
return null;
}
try {
// 是否需要检测超时
// 1.需要:poll阻塞获取,等待keepAliveTime,等待结束就返回,不管有没有获取到任务
// 2.不需要:take持续阻塞,直到获取结果
Runnable task= timed ? workQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS) : workQueue.take();
if (task!=null){
return task;
}
TimeOut = true;
} catch (InterruptedException e) {
TimeOut = false;
}
}
}
@Getter
@Setter
private class Worker implements Runnable{
private Thread thread;
private Runnable task;
public Worker(Runnable task){
this.task=task;
thread=new Thread(this);
}
@Override
public void run() {
runWorker(this);
}
}
}
测试
public class ThreadPoolTest {
public static void main(String[] args) {
//创建线程池
/**
*核心线程1个 , 最大线程2个, ‘=
* 提交4个任务,第一个任务交给核心线程,第二个进入任务队列,第三个任务交给非核心线程,第四个被拒绝
*/
ThreadPool threadPool = new ThreadPool(1, 2, 1,TimeUnit.SECONDS, new ArrayBlockingQueue(2));
threadPool.execute(new FutureTask<String>(new Runnable() {
@Override
public void run() {
System.out.println();
}
},"success"));
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("{}:执行第1个任务..."+ Thread.currentThread().getName());
sleep(10);
}
});
sleep(1);
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("{}:执行第2个任务..."+ Thread.currentThread().getName());
sleep(10);
}
});
sleep(1);
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("{}:执行第3个任务..."+ Thread.currentThread().getName());
sleep(10);
}
});
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("{}:执行第4个任务..."+ Thread.currentThread().getName());
sleep(10);
}
});
sleep(1);
System.out.println("main结束");
}
private static void sleep(int seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ThreadPoolExecutor源码分析
实际工作中,Executor、ExecutorService、AbstractExecutorService并不常见,我们一般直接使用ThreadPoolExecutor这个实现类。通常情况下,大家讨论线程池时,其实都是在讨论ThreadPoolExecutor,只不过他们自己都没意识到。
上面说到,ExecutorService接口新增了submit(),可以接收Runnable/Callable,并且支持返回值。而AbstractExecutorService则“初步”实现了submit():
submit()仅仅做了Runnable/Callable的统一包装,具体的任务执行还是交给Executor#execute()。也就是通过模板方法模式,把具体的实现交给了子类。而这个“子类”,一般就是指ThreadPoolExecutor:
execute()本身思路很明确,它安排了任务处理的总流程:
● 优先使用核心线程处理任务
● 核心线程满了,优先入队
● 等到队列满了,尝试开启非核心线程处理任务
● 如果非核心线程也满了,那就没办法了,执行拒绝策略
核心线程是如何处理任务的?
● 优先使用核心线程处理任务
● 核心线程满了,优先入队
● 等到队列满了,尝试开启非核心线程处理任务
● 如果非核心线程也满了,那就没办法了,执行拒绝策略
public void execute(Runnable command) { // 这里的command,就是submit()里封装的FutureTask
if (command == null)
throw new NullPointerException();
//(1)获取ctl。ctl用来标记线程池状态(高3位),线程个数(低29位)
int c = ctl.get();
//(2)当前线程池线程个数是否小于corePoolSize,小于则【创建核心线程】处理当前任务。
if (workerCountOf(c) < corePoolSize) {
// addWorker()第二个参数代表是否核心线程
if (addWorker(command, true))
// 注意,核心线程的创建、执行都在addWorker()方法中,一旦任务提交给核心线程,整个execute()就结束了
return;
c = ctl.get();
}
// ...
}
addWordker()方法创建线程:
● 先把task封装成Worker
● 经过一系列步骤,启动Worker里的Thread,线程开启后会执行Worker里的Task
一起来看看Worker是啥:
看上面Worker的构造器,我们不难发现 Worker = Thread + Task:
● getThreadFactory().new Thread()会创建一个线程
● Task则是Executor#execute(task)提交的那个任务
调用的流程大致是这样的:
右图Worker#run()又调用ThreadPoolExecutor#runWorker():
整个的调用链
线程池的生产消费模型
核心线程的处理逻辑虽然绕,但理清楚以后还是简单的。但从上面的介绍来看,你会发现和普通的new Thread(target).start()没太大区别,不是说线程池本质是生产消费模型吗?ThreadPoolExecutor的生产者是谁,消费者又是谁,什么时候消费、如何消费?
前面提到过,如果不断的往线程池里面添加任务,大概会经历4个阶段:
1.核心线程处理任务
2.核心线程满了,任务进入任务队列
3.任务队列满了,扩展非核心线程处理任务
4.拒绝策略
任务进入队列即为生产,而当核心线程/非核心线程处理完手头的任务,从workQueue中取出任务,就是消费。
生产消费模式是通过阻塞队列实现的,我们可以在创建ThreadPoolExecutor的时候指定阻塞队列:
生产者
我们往线程池提交任务的过程就是生产的过程:
public void execute(Runnable command) { // 这里的command,就是submit()里封装的FutureTask
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 假设核心线程数已满,跳过这一步
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// workQueue.offer(command):尝试把任务加入到workQueue(任务队列),这个workQueue就是new ThreadPoolExecutor()时指定的BlockingQueue
if (isRunning(c) && workQueue.offer(command)) {
// ...
}
else if (!addWorker(command, false))
reject(command);
}
任务进来后,先判断核心线程是否满了,如果没有满,通过new Woker()的方法创建核心线程,调用线程的start()的方法,会自动调用run()方法,而Worker重写了run()方法,在run()方法中调用runWorker()方法,此方法用来循环执行任务,并通过调用getTask()方法循环从阻塞队列中取出任务,如果没有任务,队列就会阻塞,直到任务重新进入队列。
消费者
核心/非核心线程处理完手头任务后,是如何从任务队列获取新任务的呢?还记得ThreadPoolExecutor#runWorker()吗,异步线程开启后最终会调用它。
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
// ...
try {
// 线程开启后,进入循环:当前任务不为空 || 队列任务不为空
while (task != null || (task = getTask()) != null) {
// ...
try {
// ...
task.run();
// ...
} finally {
// ...
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
addWorker()可不是简单地创建线程并执行当前任务就完事了,runWoker()内部会循环,看看队列里有没有任务需要处理(getTask).
线程的复用与销毁
一般来说new Thread().start()执行完目标任务后,就会自然销毁。那么线程池是如何做到任务跑完了之后线程不销毁的呢?
答案就在getTask()方法中:
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
// 尝试循环获取任务
for (;;) {
// 看不懂,跳过
int c = ctl.get();
int rs = runStateOf(c);
// 看不懂,跳过 Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
// 看不懂,跳过
int wc = workerCountOf(c);
// 是否需要检测超时:允许所有线程超时回收(包括核心线程) || 当前线程数超过核心线程
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 超时了,跳出循环
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
// 是否需要检测超时
// 1.需要:poll阻塞获取,等待keepAliveTime,等待结束就返回,不管有没有获取到任务
// 2.不需要:一直阻塞,直到获取结果
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
// r==null,任务为空,timedOut=true
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
也就是说,如果是核心线程,timed永远为false,那么就会调用workQueue.take()一直阻塞下去,直到有新的任务提交进来。但是处理结束后,还是会进入循环,周而复始。由于线程永远处于阻塞等待任务、执行任务、继续阻塞等待任务的死循环中,也就永远不会销毁了。
线程池之所以能复用,仅仅是让线程进入阻塞状态罢了。
销毁
特别说明一下:
核心线程不会被销毁,并不是指的是特定的线程不会被销毁,而是线程无论怎么销毁,最终会保持线程池中的线程数不小于核心线程数;
而所说的线程销毁,就是任务执行完成之后,继续往下走,和new Thread().start()一样了。
如果不考虑allowCoreThreadTimeOut(一般不会刻意设置为true):
● 那么当线程数不超过corePoolSize时,每一个线程都是核心线程,此时并不需要进行“超时检测”,所以线程会直接调用BlockingQueue#take()阻塞等待,直到有新的任务被提交。即使跳出getTask(),回到runWorker()执行完新的任务,也别指望线程就这么结束了,因为runWorker()本身也是循环,又会回到getTask()…从宏观上来看,就形成了所谓的“线程池Thread复用”
● 当前线程数超过corePoolSize,那么就会getTask()里的循环就会进行“超时检测”。所谓的超时检测,其实就是“阻塞等待keepAliveTime”,等待结束直接返回,不论是否拿到任务。假设任务为null,就会跳过if判断,设置timeOut=true(线程当前数超过了corePoolSize && 这次又取不到任务,说明啥?线程池没任务,空闲了,所以施主你该走了)
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
// 尝试循环获取任务
for (;;) {
// 省略部分代码...
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 此时条件成立
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
// 执行成功
if (compareAndDecrementWorkerCount(c))
// 返回空任务,跳出当前循环。而外层runWorker()由于 task==null,也会跳出循环
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
小结
如果把FutureTask也算上,实际上有4种执行多线程任务的方式:
● Thread:
○ 重写Thread的run()
○ 通过Thread的构造器传入Runnable实例
○ 通过Thread的构造器传入Runnable实例(FutureTask,内部包装了Runnable/Callable)
● 线程池:
○ 通过线程池(Runnable/Callable都行)
Thread和线程池看起来是单个线程和线程群体的关系,但实际上Thread和线程池还有个连接点:FutureTask。无论是Thread还是线程池,都可以接收FutureTask,只不过Thread使用FutureTask时需要我们在外部自己包装好Runnable和Callable,而线程池把这个操作内化了。
shutdown和shutdownNow
当我们频繁使用线程的时候,为了节约资源快速相应需求,我们会考虑用线程池,而线程池使用之后,就需要关闭,关闭一般会使用shutdown和shutdownNow:
区别
从字面意思就能理解,shutdownNow()能立即停止线程池,正在跑的和正在等待的任务都停下了。这样做立即生效,但是风险也比较大;
shutdown()只是关闭了提交通道,用submit()是无效的;而内部该怎么跑还是怎么跑,跑完再停。
shutdown
将线程池状态置为SHUTDOWN,并不会立即停止:
停止接收外部submit的任务
内部正在跑的任务和队列里等待的任务,会执行完
等到第二步完成后,才真正停止
shutdownNow
将线程池状态置为STOP。企图立即停止,事实上不一定:
跟shutdown()一样,先停止接收外部提交的任务
忽略队列里等待的任务
尝试将正在跑的任务interrupt中断
返回未执行的任务列表
终止线程的方法是通过调用Thread.interrupt()方法来实现的,但是大家知道,这种方法的作用有限,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它也可能必须要等待所有正在执行的任务都执行完成了才能退出。但是大多数时候是能立即退出的。
总结
优雅的关闭,用shutdown()
想立马关闭,并得到未执行任务列表,用shutdownNow()