起因
一直听说java的线程池能减少线程创建的开销,是多线程编程中的一把利器。但是,具体线程池是个啥东东,为啥这么牛逼,又是怎么做到的减少开销,一直都是处于知其然不知其所以然的状态。最近正好面试复习这些知识,干脆打算彻底搞懂它。
总结
哈哈,上来先总结一句,装装逼。ThreadPoolExecutor是java线程池的核心类,对其进行分析还是挺长的,先总结一句,能有个整体的把握。一句话来概述,线程池就是用一堆包装住Thread的Wroker类的集合,在里面有条件的进行着死循环,从而可以不断接受任务来进行。
问题
先抛出几个问题,带着问题进行分析会更有目的性。
- 问题一:线程池的池子是啥,里边放了什么
- 问题二:线程池能减少开销,它是怎么做到的
- 问题三:线程池会自动往池子里创建线程,它会自动把线程释放掉吗?如果能,是怎么做到的;如果不能,它不怕溢出吗
- 问题四:线程池怎么使用,java给除了怎样的工具集。
解析
线程
首先,java为多线程提供了Thread和Runnable(还有个能有返回值的Callable),Runnable和Callable的真正执行还是Thread。Thread不外就是两个方法start和run。
|
|
从这个方法解释上看,start()这个方法,最终会交给VM 去执行run()方法,所以一般情况下,我们在随便一个线程上执行start(),里面的run()操作都会交给VM 去执行。
第一部分:ThreadPoolExecutor的继承结构
根据源码可以知道,ThreadPoolExecutor是继承的AbstractExecutorService(抽象类)。再来看一下AbstractExecutorService的结构可以发现,AbstractExecutorService实现了ExecutorService,并且ExecutorService继承Executor接口。
如下是Executor和ExecutorService接口中一些方法:
|
|
|
|
可以简单总结一下:Executor这个接口只有一个方法execute(Runnable command)(以Command Pattern(命令模式)的设计模式实现);在ExecutorService接口中一部分是和执行器生命周期相关的方法,而另一部分则是以各种方式提交要执行的任务的方法。像submit()就是提交任务的一个方法,在实现中做了适配的工作,无论参数是Runnable还是Callable,执行器都会正确执行。ExecutorService中,和生命周期相关的,声明了5个方法:
- awaitTermination() 阻塞等待shutdown请求后所有线程终止,会有时间参数,超时和中断也会令方法调用结束
- isShutdown() 通过ctl属性判断当前的状态是否不是RUNNING状态
- isTerminated() 通过ctl属性判断当前的状态是否为TERMINATED状态
- shutdown() 关闭Executor,不再接受提交任务
- shutdownNow() 关闭Executor,不再接受提交任务,并且不再执行入队列中的任务
那么再来看一下AbstractExecutorService,这个类是ExecutorService的一个抽象实现。其中,提交任务的各类方法已经给出了十分完整的实现。之所以抽象,是因为和执行器本身生命周期相关的方法在此类中并未给出任何实现,需要子类扩展完善(模板方法设计模式)拿一个submit方法出来分析一下:
|
|
从代码可以看出实际上用到的是RunnableFuture的实现类FutureTask。但最终还是调用了execute()方法,在子类中实现。
在正式进入ThreadPoolExecutor源码分析之前还需要补充一点的是:Executors(工厂方法设计模式)
java.util.concurrent.Executors是个工具类,提供了很多静态的工具方法。其中很多对于执行器来说就是初始化构建用的工厂方法。
- 重载实现的newFixedThreadPool()
- 重载实现的newSingleThreadExecutor()
- 重载实现的newCachedThreadPool()
- 重载实现的newSingleThreadScheduledExecutor()
- 重载实现的newScheduledThreadPool()
这些方法返回的ExecutorService对象最终都是由ThreadPoolExecutor实现的,根据不同的需求以不同的参数配置,或经过其它类包装。其中,Executors中的一些内部类就是用来做包装用的。Executors类中还有静态的defaultThreadFactory()方法,当然也可以自己实现自定义的ThreadFactory。
第二部分:ThreadPoolExecutor源码分析
下面正式进入ThreadPoolExecutor:(按照程序运行顺序分析)
1、ThreadPoolExecutor的全参数构造方法:
|
|
根据注释:
- corePoolSize 是线程池的核心线程数,通常线程池会维持这个线程数
- maximumPoolSize 是线程池所能维持的最大线程数
- keepAliveTime 和 unit 则分别是超额(空闲)线程的空闲存活时间数和时间单位
- workQueue 是提交任务到线程池的入队列
- threadFactory 是线程池创建新线程的线程构造器
- handler 是当线程池不能接受提交任务的时候的处理策略
2、execute方法提交任务
|
|
通过注释:提交新任务的时候,如果没达到核心线程数corePoolSize,则开辟新线程执行。如果达到核心线程数corePoolSize, 而队列未满,则放入队列,否则开新线程处理任务,直到maximumPoolSize,超出则丢弃处理。同时判断目前线程的状态是不是RUNNING其他线程有可能调用了shutdown()或shutdownNow()方法,关闭线程池,导致目前线程的状态不是RUNNING。在上面提交任务的时候,会出现开辟新的线程来执行,这会调用addWorker()方法。
3、addWorker方法
|
|
第一部分:第一段从第3行到第26行,是双层无限循环,尝试增加线程数到ctl变量,并且做一些比较判断,如果超出线程数限定或者ThreadPoolExecutor的状态不符合要求,则直接返回false,增加worker失败。第二部分:从第28行开始到结尾,把firstTask这个Runnable对象传给Worker构造方法,赋值给Worker对象的task属性。Worker对象把自身(也是一个Runnable)封装成一个Thread对象赋予Worker对象的thread属性。锁住整个线程池并实际增加worker到workers的HashSet对象当中。成功增加后开始执行t.start(),就是worker的thread属性开始运行,实际上就是运行Worker对象的run方法。Worker的run()方法实际上调用了ThreadPoolExecutor的runWorker()方法。在看runWorker()之前先看一下Worker对象。
4、Worker对象
Worker是真正的任务,是由任务执行线程完成,它是ThreadPoolExecutor的核心。每个线程池中,有为数不等的Worker对象,每个Worker对象中,包含一个需要立即执行的新任务和已经执行完成的任务数量,Worker本身,是一个Runnable对象,不是Thread对象它内部封装一个Thread对象,用此对象执行本身的run方法,而这个Thread对象则由ThreadPoolExecutor提供的ThreadFactory对象创建新的线程。(将Worker和Thread分离的好处是,如果我们的业务代码,需要对于线程池中的线程,赋予优先级、线程名称、线程执行策略等其他控制时,可以实现自己的ThreadFactory进行扩展,无需继承或改写ThreadPoolExecutor。)
|
|
5、runWorker()方法
|
|
根据代码顺序看下来,其实很简单。
- 线程开始执行前,需要对worker加锁,完成一个任务后执行unlock()
- 在任务执行前后,执行beforeExecute()和afterExecute()方法
- 记录任务执行中的异常后,继续抛出
- 每个任务完成后,会记录当前线程完成的任务数
- 当worker执行完一个任务的时候,包括初始任务firstTask,会调用getTask()继续获取任务,这个方法调用是可以阻塞的
- 线程退出,执行processWorkerExit(w, completedAbruptly)处理
接下来看一下getTask()是怎样实现空闲线程复用的
6、getTask()方法
|
|
getTask()实际上是从工作队列(workQueue)中取提交进来的任务。这个workQueue是一个BlockingQueue,通常当队列中没有新任务的时候,则getTask()会阻塞。另外,还有定时阻塞这样一段逻辑:如果从队列中取任务是计时的,则用poll()方法,并设置等待时间为keepAlive,否则调用阻塞方法take()。当poll()超时,则获取到的任务为null,timeOut设置为 true。这段代码也是放在一个for(;;)循环中,前面有判断超时的语句,如果超时,则return null。这意味着runWorker()方法的while循环结束,线程将退出,执行processWorkerExit()方法。
其中:
|
|
即判断当前线程池的线程数是否超出corePoolSize,如果超出这个值并且空闲时间多于keepAlive则当前线程退出。另外一种情况就是allowCoreThreadTimeOut为true,就是允许核心在空闲超时的情况下停掉。最后再来看一下线程池线程数的维护和线程的退出处理。
7、processWorkerExit()方法
|
|
这个方法最主要就是从workers的Set中remove掉一个多余的线程。这个方法的第二个参数是判断是否在runWorker()中正常退出了循环向下执行。当前如果不是,说明在执行任务的过程中出现了异常,completedAbruptly为true,线程直接退出,需要直接对活动线程数减1 ;如果是正常退出则加锁统计完成的任务数,并从workers这个集合中移除当前worker。执行tryTerminate(),这个方法后面会介绍,主要就是尝试将线程池推向TERMINATED状态。最后比较当前线程数是不是已经低于应有的线程数,如果这个情况发生,则添加无任务的空Worker到线程池中待命。以上,增加新的线程和剔除多余的线程的过程大概就是如此,这样线程池能保持额定的线程数,并弹性伸缩,保证系统的资源不至于过度消耗。以上,增加新的线程和剔除多余的线程的过程大概就是如此,这样线程池能保持额定的线程数,并弹性伸缩,保证系统的资源不至于过度消耗。
8、tryTerminate()方法
tryTerminate()的意义就在于尝试进入终止状态
|
|
当ctl中worker数字为0时执行terminated()方法,否则等锁中断一个空闲的Worker,其中interruptIdleWorkers()就是来中断线程的。空闲的worker主要是通过worker的tryLock()来确认的,因为执行任务的worker互斥地锁定对象。中断worker导致线程退出,最终还会循环尝试终止其它的空闲线程,直到整个ThreadPoolExecutor最后终结。到此为止,从线程池的新建,提交任务,到结束,基本结束。
第三部分:ThreadPoolExecutor生命周期
下面来补充一下ThreadPoolExecutor生命周期中的一些重要方法的介绍:
1、ShutDown()方法
|
|
尝试将状态切换到SHUTDOWN,这样就不会再接收新的任务提交。对空闲线程进行中断调用。最后检查线程池线程是否为0,并尝试切换到TERMINATED状态。
2、ShutDownNow()方法
|
|
主要所做的事情就是切换ThreadPoolExecutor到STOP状态,中断所有worker,并将任务队列中的任务取出来,不再执行。最后尝试修改状态到TERMINATED。
shutdown()和shutdownNow()的区别:
shutdown()新的任务不会再被提交到线程池,但之前的都会依旧执行,通过中断方式停止空闲的(根据没有获取锁来确定)线程。
shutdownNow()则向所有正在执行的线程发出中断信号以尝试终止线程,并将工作队列中的任务以列表方式的结果返回。
- 一个要将线程池推到SHUTDOWN状态,一个将推到STOP状态
- 并且对运行的线程处理方式不同,shutdown()只中断空闲线程,而shutdownNow()会尝试中断所有活动线程
- 还有就是对队列中的任务处理,shutdown()队列中已有任务会继续执行,而shutdownNow()会直接取出不被执行
- 相同的是都在最后尝试将线程池推到TERMINATED状态。
3、awaitTermination()方法
|
|
阻塞等待shutdown请求后所有线程终止,会有时间参数,超时和中断也会令方法调用结束。实际所做的就是Condition的定时await调用。用于状态依赖的线程阻塞。
4、ThreadPoolExecutor生命周期的扩展点
在生命周期上,ThreadPoolExecutor为扩展的类提供了一些扩展点,这是很好的设计,对扩展开放。其中声明了如下protected的方法:
- beforeExecute() 在每个任务执行前做的处理
- afterExecute() 在每个任务执行后做的处理
- terminated() 在ThreadPoolExecutor到达TERMINATED状态前所做的处理
- finalize() 有默认实现,直接调用shutdown(),以保证线程池对象回收
- onShutdown() 在shutdown()方法执行到最后时调用,在ScheduledThreadPoolExecutor类实现中用到了这个
- 扩展点,做一些任务队列的清理操作。
第四部分:ThreadPoolExecutor的丢弃–RejectedExecutionHandler
当ThreadPoolExecutor执行任务的时候,如果线程池的线程已经饱和,并且任务队列也已满。那么就会做丢弃处理,这也是execute()方法实现中的操作,源码如下:
|
|
RejectedExecutionHandler 其中只有rejectedExecution()一个方法。返回为void,而参数一个是具体的Runnable任务,另一个则是被提交任务的ThreadPoolExecutor。ThreadPoolExecutor给出了4种基本策略的实现。分别是:
- CallerRunsPolicy
- AbortPolicy
- DiscardPolicy
- DiscardOldestPolicy
|
|
根据源码可知:
调用者执行策略(CallerRunsPolicy)
在这个策略实现中,任务还是会被执行,但线程池中不会开辟新线程,而是提交任务的线程来负责维护任务。会先判断ThreadPoolExecutor对象的状态,之后执行任务。这样处理的一个好处,是让caller线程运行任务,以推迟该线程进一步提交新任务有效的缓解了线程池对象饱和的情况。废弃终止(AbortPolicy)
不处理,而是抛出java.util.concurrent.RejectedExecutionException异常。
注意,处理这个异常的线程是执行execute()的调用者线程。直接丢弃(DiscardPolicy)
这个也是实现最简单的类,其中的rejectedExecution()方法是空实现,即什么也不做,那么提交的任务将会被丢弃,而不做任何处理丢弃最老(DiscardPolicy)
会丢弃掉一个任务,但是是队列中最早的。注意,会先判断ThreadPoolExecutor对象是否已经进入SHUTDOWN以后的状态。之后取出队列头的任务并不做任何处理,即丢弃,再重新调用execute()方法提交新任务。
回答问题
此时,我们就可以回答之前提出的问题了。
- 问题一:线程池的池子是啥,里边放了什么
池子其实就是HashSet,每个Worker包装了一个Thread和一个Runnable任务,由thread来执行runnable任务。 - 问题二:线程池能减少开销,它是怎么做到的
线程池减少开销的原理其实就是减少线程创建和销毁的开销。在线程池中有常驻线程,这些线程会去阻塞队列中不断请求新任务。当我们创建一个新任务的时候,可以直接丢到阻塞队列中,等线程有时间的时候就会来执行这个任务,而不用每次都创建新的线程。所以线程池其实比较适合需要经常执行不同临时任务的场景,比如网络请求,用户不是每时每刻都在与服务器交互,但是用户量非常大,请求的特点就是短暂而大量。 - 问题三:线程池会自动往池子里创建线程,它会自动把线程释放掉吗?如果能,是怎么做到的;如果不能,它不怕溢出吗
这个看线程池的配置,如果是配置了超时时间,那么一个线程在阻塞队列等待超过一定时间之后就会自己退出,这部分逻辑在runWorker()的getTask()部分。如果线程是计时的,在队列中取任务是用的poll(),这个会在超时之后返回null,然后把timedout设置为true,这样就会在返回到runWorker()方法的时候退出循环,执行processWorkerExit方法来结束当前线程。
另外,线程池会配置一个corePoolSize,代表线程池中的核心线程个数。一般来说,核心线程一般不是可以计时的,可以常驻。核心线程在阻塞队列中等待任务的时候使用的方法是take()。 问题四:线程池怎么使用,java给除了怎样的工具集。
给一个demo1234567891011121314151617181920212223242526272829303132public class Test {public static void main(String[] args) {ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,new ArrayBlockingQueue<Runnable>(5));for(int i=0;i<15;i++){MyTask myTask = new MyTask(i);executor.execute(myTask);System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+executor.getQueue().size()+",已执行玩别的任务数目:"+executor.getCompletedTaskCount());}executor.shutdown();}}class MyTask implements Runnable {private int taskNum;public MyTask(int num) {this.taskNum = num;}@Overridepublic void run() {System.out.println("正在执行task "+taskNum);try {Thread.currentThread().sleep(4000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("task "+taskNum+"执行完毕");}}
执行结果:
|
|