为什么需要线程池:
- 重复利用池中的线程避免了每次新建和销毁线程的开销
- 加快任务速度,由于池中线程早已存在
- 提高线程的可管理性。无限制的创建线程终究会达到系统上限并且不利于对线程使用的监控。
Executor框架概览:
从左往右,从上往下开始介绍:
Executor: 该接口只有一个void execute(Runnable command)方法,这么做是将任务的提交与执行分离。
ExecutorService: 扩展了Executor接口的功能,提供了任务以及线程池管理的一些方法,例如submit,shutdown
AbstractExecutorService: ExecutorService执行方法的默认实现
ThreadPoolExecutor: 线程池,可以通过调用Executors以下静态工厂方法来创建线程池并返回一个ExecutorService对象。JDK提供的一些线程池例如newFixedThread,newCacheThreadPool就是使用它来实现的。
ScheduledExecutorService: 一个可定时调度任务的接口
ScheduledThreadPoolExecutor: ScheduledExecutorService的实现,一个可定时调度任务的线程池
常用的一些线程池例如:FixedThreadPool/cachedThredPool/singleThreadPool都是基于ThreadPoolExecutor来实现的,newScheduledThreadPool使用的是SchedulerThreadPoolExecutor底层也是ThreadPoolExecutor,所以有必要来分析一波ThreadPoolExecutor。
ps: 有一个例外:,newWorkStealingPool使用的是ForkJoinPool。(这个如何运行后续再分析吧)
ThreadPoolExecutor源码分析
线程池是如何执行任务的?:
execute(Runnable command)–(callable任务咋办,分析FutureTask的时候就知道了)
这里的ctl是一个AtomicInteger变量,头三位用于表示线程池运行状态,后29位用于表示workerCount数量,所以线程池的数量最多有 2^29-1 个。
- 如果输入的command为null,则直接抛异常。
- 如果当前运行的线程数少于corePoolSize,则通过addWorker(command,true)创建一个新的线程来执行任务,这一步需要获取全局锁。也就是说,线程池一开始就是要尽快开启corePoolSize个线程。
- 否则,如果线程池此时还在运行状态,那么就将任务加入等待队列。成功的话会进行一次双重检查:如果此时线程池不处于运行状态的话,那就会将任务移除workQueue队列并调用reject策略;如果线程池处于运行状态但是此时池中的正在运行的线程数为0,那么就要增加一个Worker。(原因是此时Pool有可能被shutdown,shutdown()可能会把所有的线程都停了,这个会在后面shutdown那里讲到)。
---- 这里注意,如果线程池中已经开了corePoolSize个线程,但是任务还是会被提交到workQueue中。
- 如果此时等待队列已满,那就需要再一次增加线程数了,最多增加到maxPoolSize。如果addWorker()失败,那么就调用Reject策略,如果此时任务已经加入队列的话会把任务移除。
流程图如下所示:
图中,其实每一步都会检查线程池的运行状态,上面1和4之所以没有是因为addWorker()方法内部做了线程池运行状态的检查。有两点需要提前了解下:
1.当线程池处于 SHUTDOWN ,不允许提交任务,但是已有任务继续执行(shutdown()),并且firstTask==null && !workQueue.empty() 还允许创建新的线程(之所以允许,这里在提一下就是shutdown()可能会将所有的线程全部停掉,这个时候就要创建一个firstTask为null的线程,然后执行队列中的任务)。
2.当状态大于 SHUTDOWN ,不允许提交任务,且中断正在执行任务 (shutdownNow())
addWorker()是怎么增加线程的:
看起来核心是addWorker()这个方法,这个方法是怎么增加线程的?等待队列里的任务又是怎么取出来运行的?
线程在线程池中被封装成了Worker这个类,稍后再看下Worker这个类,先分析下addWorker()这个方法:
检查线程池状态以及有没有达到线程数量的限制,如果此时线程池未关闭,则使用CAS方法将正在运行的线程数加1:即workerCount+1。之所以使用CAS是因为可能有在并发的增加线程数量。
(ps:再注意下,如果线程池状态为SHUTDOWN,firstTask==null,并且workQueue不为空,是可以创建线程的)
接着就可以开始创建线程来执行任务了,代码如下:
新建一个Worker对象,更新largestPoolSize大小并将worker保存到一个HashSet中(涉及到修改线程的操作需要获取mainLock)。
最后调用Thread的start()方法启动线程执行任务。
如果失败的话,会调用addWorkerFailed()逻辑,将workCount减1并做一些其他清理工作。
但是看到这里,还是没找找到Worker是怎么执行firstTask的,又是怎么去从队列中拿取任务来执行的?
addWorker()中,Worker是怎么获取任务然后执行的:
立刻想到,既然Worker是线程,那么肯定有run()方法,看下Worker的结构:
private final class Worker
extends AbstractQueuedSynchronizer implements Runnable
发现它自己继承了AQS,说明Worker是一把锁(注意AQS不可重入)。在它的run()中调用了runWorker(Worker w):
首先,如果task没有传入的话,那么就使用getTask()去阻塞队列中获取。(不断循环的去从阻塞队列中获取,直到队列中的任务全部执行完)
接着对当前Worker加锁,并且每次执行task前都会检查一次线程池的状态。加锁是为了当前线程为其他行为中断,即不被shutdow()之类的操作中断,中断由线程自己执行。如果是STOP,要中断(Interrupt)所有线程;如果不为STOP,要保证所有线程没有被中断。
接着就调用task.run()执行任务了,执行用户的任务了。
任务全部执行完成之后(即获取到的task==null),则调用processWorkerExit()方法关闭该Worker。
ps: 如果线程在获取任务的过程中线程意外终止了,会执行到processWorkerExit()会创建一个补偿的Worker线程。目前个人想到的是在shutdown状态下,队列中还有任务,获取任务的时候由于超时时间已到,把线程给interrupt了,此时在改方法里面新建一个补偿线程
逻辑差不多理清楚了,再来看下Worker从队列中获取任务的getTask()方法:
个人猜测多个线程从同一个BlockQueue中获取任务的话需要用到锁吧…
- 如果线程的状态>=STOP,workerCount--,然后返回null。(之前说过,返回null表示没有任务需要执行了),Worker会走关闭的逻辑。
- 如果超时时间到了,且队列为空,操作很上面的一样。
3.阻塞直到获取到一个Task。获取时分为有超时时间限制和阻塞直到获取完毕两种方式,至于阻塞队列那边是怎么执行的,到时候看阻塞队列的文章吧。
最开始留了一个问题,线程池是怎么执行Callable任务的,Callable任务结果是存到哪的啊?
再找找看,从提交任务的submit()接口看进去:
从方法中可以看出,代码是将task封装成一个FutureTask对象仍到线程中执行的,我们看下它的run()方法:
其实就是先调用call()执行任务,然后再调用set将结果设置到某个地方。set()方法如下:
- 首先是将任务的状态设置为COMPLETING
- 然后将任务的返回结果赋值给outcome,再讲状态设置成NORMAL,此时就可以让别的线程get()到结果了
- 执行finishCompletion(),唤醒等待队列中的线程,让它们获取结果。
我们再来看下FutureTask 的get()方法,这样就可以了解获取数据的线程是怎么被阻塞的了:
代码其实很简单,如果调用get()方法的线程检测到task未完成,那么就调用awaitDone()阻塞在这里,否则就调用report(s)返回任务结果。
awaitDone(boolean timed, long nanos)运行逻辑如下:
report(s)方法逻辑如下:
其实就是当Task状态为NORMAL的时候返回结果,否则就抛出相应的异常。
到这里,我们就明白线程池是如何执行任务的,并且执行Callable任务的时候是怎么获取到返回结果的了。
最后回过来分析下线程池是如何关闭的:
Shutdown():
其中步骤3是尝试获取Worker的锁,然后使用interrupt中断线程。因为之前分析过,Worker除了正在执行task的过程中,有两种情况下不持有锁:
一是任务完成之后,再一次从队列中获取任务的时候
二是没有任务需要执行的时候
这个方法有可能会把所有的线程都关掉,但是此时可能队列中还有任务,所以你现在应该明白了,为什么execute()那里会有个addWorker(null,false)的方法。就是为了在误关全部线程的情况下,可以再开启线程,执行队列中未完成的任务。
步骤4的tryTerminate()是关闭所有线程,并通知所有等待的线程,这些等待的线程是调用了awaitTermination()方法的线程。
至此,线程池就关闭了。
shutdownNow():
和shutdown差不多,只是将state置位STOP状态,还是调用停止空闲线程的方法。唯一不同的是它会返回队列中未执行的任务。
如果是STOP的状态,及时workQueue不为空,execute()方法也不会再新开Worker执行任务了。
至此,线程池完全关闭了。
补充一些细节:
线程池的状态共有五种:
RUNNING | 接受新提交的任务并且处理任务队列中的任务 |
SHUTDOWN | 不接受新提交的任务,但是处理任务队列中的任务 |
STOP | 不接受新提交的任任务,不处理任务队列中的任务,同时中断正在运行中的任务 |
TIDYING | 所有任务被终止, 活动的线程数workCount为0,此状态下还会执行terminated钩子方法 |
TERMINATED | terminated钩子方法已执行 |
越小越稳定!
参考:
https://cloud.tencent.com/developer/article/1124439