文章目录
java并发包中线程池ThreadPoolExecutor原理探究
一、介绍
线程池主要解决两个问题:一是当执行大量异步任务时线程池能够提供较好的性能。在不使用线程池时,每当需要执行异步任务时直接new一个线程来运行,而线程的创建和销毁是需要开销的。线程池里面的线程是可复用的,不需要每次执行异步任务时都重新创建和销毁线程。二是线程池提供了一种资源限制和管理手段,比如可以限制线程的个数,动态新增线程等。每个ThreadPoolExecutor也保留了一些基本的统计数据,比如当前线程池完成的任务数目等。
另外,线程池也提供了许多可调参数和可扩展接口,以满足不同情境的需要,程序员可以使用更方便的Executors的工厂方法,比如newCachedThreadPool(线程池线程个数最多可达Integer.MAX_VALUE,线程自动回收)、newFixedThreadPool(固定大小的线程池)和newSingleThreadExecutor(单个线程)等来创建线程池,当然用户还可以自定义。
二、类图介绍
如下图所示类图种,Executors其实是个工具类,里面提供了好多静态方法,这些方法根据用户选择返回不同的线程池实例。ThreadPoolExecutor继承了AbstractExecutorService,成员变量ctl是一个Integer的原子变量,用来记录线程池状态和线程池种线程个数,类似ReentrantReadWriteLock使用一个变量来保存两种信息。
这里假设Integer类型时32位二进制表示,则其中高三位用来表示线程池状态,后面29位用来记录线程池线程个数。
线程池状态:
线程池状态含义如下。
- RUNNING:接受新任务并且处理阻塞队列里的任务。
- SHUTDOWN:拒绝新任务但是处理阻塞队列里的任务。
- STOP:拒绝新任务并且抛弃阻塞队列里的任务,同时会中断正在处理的任务。
- TIDYING:所有任务都执行完(包含阻塞队列里面的任务)后当前线程池活动线程数为0,将要调用terminated方法。
- TERMINATED:终止状态。terminated方法调用完成以后的状态。
线程池转换列举如下。
- RUNNING -> SHUTDOWN:显式调用shutdown()方法,或者隐式调用了finalize()方法里面的shutdown()方法。
- RUNNING或SHUTDOWN -> STOP:显式调用shutdownNow()方法时。
- SHUTDOWN -> TIDYING:当线程池和任务队列都为空时。
- STOP -> TIDYING:当线程池为空时。
- TIDYING -> TERMINATED:当terminated() hook方法执行完成时。
线程池参数如下。
- corePoolSize:线程池核心线程个数。
- workQueue:用于保存等待执行的任务的阻塞队列,比如基于数组的有界ArrayBlockingQueue、基于链表的无界LinkedBlockingQueue、最多只有一个元素的同步队列SynchronousQueue以及优先级队列PriorityBlockingQueue等。
- maximunPoolSize:线程池最大线程数量。
- ThreadFactory:创建线程的工厂。
- RejectedExecutionHandler:饱和策略,当队列满并且线程个数达到maximunPoolSize后采取的策略,比如AbortPolicy(抛出异常)、CallerRunsPolicy(使用调用者所在线程来运行任务)、DiscardOldestPolicy(调用poll丢弃一个任务,执行当前任务)及DiscardPolicy(默默丢弃,不抛出异常)
- keepAliveTime:存活时间。如果当前线程池中的线程数量比核心线程数量多,并且是闲置状态,则这些闲置的线程能存活的最大时间。
- TimeUnit:存活时间的时间单位。
线程池类型如下。
- newFixedThreadPool:创建一个核心线程个数和最大线程个数都为nThreads的线程池,并且阻塞队列长度为Integer.MAX_VALUE。keepAliveTIme = 0说明只要线程个数比核心线程个数多并且当前空闲则回收。
- newSingleThreadExecutor:创建一个核心线程个数和最大线程个数都为1的线程池,并且阻塞队列长度为Integer.MAX_VALUE。keepAliveTime=0说明只要线程个数比核心线程个数多并且当前空闲则回收。
- newCachedThreadPool:创建一个按需创建线程的线程池,初始线程个数为0,最多线程个数为Integer.MAX_VALUE,并且阻塞队列为同步队列。keepAliveTIme=60说明只要当前线程在60s内空闲则回收。这个类型的特殊之处在于,加入同步队列的任务会被马上执行,同步队列里面最多只有一个任务。
如上ThreadPoolExecutor类图所示,其中mainLock是独占锁,用来控制新增Worker线程操作的原子性。termination是该锁对应的条件队列,在线程调用awaitTermination时用来存放阻塞的线程。
Worker继承AQS和Runnable接口,是具体承载任务的对象。Worker继承了AQS,自己实现了简单不可重入独占锁,其中state=0表示锁未被获取状态,state=1表示锁已经被获取的状态,state=-1是创建Worker时默认的状态,创建时状态设置为-1是为了避免该线程在运行runWorker()方法前被中断,下面会具体讲解。其中变量firstTask记录该工作线程执行的第一个任务,thread是具体执行任务的线程。
DefaultThreadFactory是线程工厂,newThread方法是对线程的一个修饰。其中poolNumber是个静态的原子变量,用来统计线程工程的个数,threadNumber用来记录每个线程工厂创建了多少个线程,这两个值也作为线程池和线程的名称的一部分。
三、源码分析
1、public void execute(Runnable command)
execute方法的作用是提交任务command到线程池进行执行。用户线程提交任务到线程池的模型图如下:
从该图可以看出,ThreadPoolExecutor的实现实际是一个生产消费模型,当用户添加任务到线程池时相当于生产者生产元素,workers线程工作集中的线程直接执行任务或者从任务列表里面获取任务时则相当于消费者消费元素。
用户线程提交任务的execute方法具体代码如下:
代码(3)判断如果当前线程池中线程个数小于corePoolSize,会像workers里面新增一个核心线程(core线程)执行该任务。
如果当前线程池中线程个数大于等于corePoolSize则执行代码(4)。如果当前线程池处于RUNNING状态则添加当前任务到任务队列。这里需要判断线程池状态是因为有可能线程池已经处于非RUNNING状态,而在非RUNNING状态下是要抛弃新任务的。
如果向任务队列添加任务成功,则代码(4.2)对线程状态进行二次校验,这是因为添加任务到任务队列后,执行代码(4.2)前有可能线程池的状态已经变了。这里进行二次校验,如果当前线程池状态不是RUNNING了则把任务从任务队列中移除,移除后执行拒绝策略;如果二次校验通过,则执行代码(4.3)重新判断当前线程池里面是否还有线程,如果没有则新增一个线程。
如果代码(4)添加任务失败,则说明任务队列已满,那么执行代码(5)尝试新开启线程来执行该任务,如果当前线程池中线程个数 > maximumPoolSize则执行拒绝策略。
代码比较长,主要分两个部分:第一部分双重循环的目的是通过CAS操作增加线程数;第二部分主要是把并发安全的任务添加到workers里面,并且启动任务执行。
首先分析第一部分代码(6)。
展开!运算后等价于
也就是说代码(6)在下面几种情况下会返回false:
- (I)当前线程池状态为STOP、TIDYING或TERMINATED。
- (II)当前线程池状态为SHUTDOWN并且已经有了第一个任务。
- (III)当前线程池状态为SHUTDOWN并且任务队列为空
内层循环的作用是使用CAS操作增加线程数,代码(7.1)判断如果线程个数超限则返回false,否则执行代码(7.2)CAS操作设置线程个数,CAS成功则退出双重循环,CAS失败则执行代码(7.3)看当前线程池的状态是否变化了,如果变了,则再次进入外层循环重新获取线程池状态,否则进入内层循环继续进行CAS尝试。
执行到第二部分的代码(8)时说明使用CAS成功地增加了线程个数,但是现在任务还没开始执行。这里使用全局的独占锁来控制把新增的Worker添加到工作集workers中。代码(8.1)创建了一个工作线程Worker。
代码(8.2)获取了独占锁,代码(8.3)重新检查线程池状态,这是为了避免在获取锁前其他线程调用了shutdown关闭了线程池。如果线程池已经被关闭,则释放锁,新增线程失败,否则执行代码(8.4)添加工作线程到线程工作集,然后释放锁。代码(8.5)判断如果新增工作线程成功,则启动工作线程。
2、工作线程Worker的执行
用户线程提交任务到线程池后,由Worker来执行。先看下Worker的构造函数。
在构造函数内首先设置Worker的状态为-1,这是为了避免当前Worker在调用runWorker方法前被中断(当其他线程调用了线程池的shutdownNow时,如果Worker状态 >= 0则会中断该线程)。这里设置线程的状态为-1,所以该线程就不会被中断了。在如下runWorker代码中,运行代码(9)时会调用unlock方法,该方法把status设置为了0,所以这时候调用shutdownNow会中断Worker线程。
在如上代码(10)中,如果当前taks==null或者调用getTask从任务队列获取的任务返回null,则跳转到代码(11)执行。如果task不为null则执行代码(10.1)获取工作线程内部特有的独占锁,然后执行扩展接口代码(10.2)在具体任务执行前做一些事情。代码(10.3)具体执行任务,代码(10.4)在任务执行完毕后做一些事情,代码(10.5)统计当前Worker完成了多少个任务,并释放锁。
这里在执行具体任务期间加锁,是为了避免在任务运行期间,其他线程调用了shutdown后正在执行的任务被中断(shutdown只会中断当前被阻塞挂起的线程)
代码(11)执行清理任务,其代码如下。
在如上代码中,代码(11.1)统计线程池完成任务个数,并且在统计前加了全局锁。把在当前工作线程中完成的任务累加到全局计数器,然后从工作集中删除当前Worker。
代码(11.2)判断如果当前线程池状态时SHUTDOWN并且工作队列为空,或者当前线程池状态时STOP并且当前线程池里面没有活动线程,则设置线程池状态为TERMINATED。如果设置为了TERMINATED状态,则还需要调用条件变量termination的signalAll方法激活所有因为调用线程池的awaitTermination方法而被阻塞的线程。
代码(11.3)则判断当前线程池里面线程个数是否小于核心线程个数,如果是则新增一个线程。
3、shutdown操作
调用shutdown方法后,线程池就不会再接受新的任务了,但是工作队列里面的任务还是要执行的。该方法会立刻返回,并不等待队列任务完成再返回。
在如上代码中,代码(12)检查看是否设置了安全管理器,是则看当前调用shutdown命令的线程是否有关闭线程的权限,如果有权限则还要看调用线程是否有中断工作线程的权限,如果没有权限则抛出SecurityException或者NullPointerException异常。
其中代码(13)的内容如下,如果当前线程池状态 >= SHUTDOWN则直接返回,否则设置为SHUTDOWN状态。
代码(14)的内容如下,其设置所有空闲线程的中断标志。这里首先加了全局锁,同时只有一个线程可以调用shutdown方法设置中断标记。然后尝试获取Worker自己的锁,获取成功则设置中断标志。由于正在执行的任务已经获取了锁,所以正在执行的任务没有被中断。这里中断的是阻塞到getTask()方法并企图从队列里面获取任务的线程,也就是空闲线程。
在如上代码中,首先使用CAS设置当前线程池状态为TIDYING,如果设置成功则执行扩展接口terminated在线程池状态变为TERMINATED前做一些事情,然后设置当前线程池状态为TERMINATED。最后调用termination.signalAll方法激活因调用条件变量termination的await系列方法而被阻塞的所有线程。