线程池ThreadPoolExecutor源码分析

 

在阿里编程规约中关于线程池强制了两点,如下:

【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决
资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或
者“过度切换”的问题。

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样
的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

-----------------------------------------------------------------

我们就来分析下ThreadPoolExecutor的代码。

首先是构造方法:

这里的参数含义想必就不用多说了,网上多的是,这里要强调的一点是,keepAliveTime这个变量的含义,很多人可能只知道这个是非核心线程空闲时的存活时间。

这句话只是说对了一小部分,通过这句话先来纠正下一些概念:

1.线程

在ThreadPoolExecutor里面是通过内部类Worker作为任务执行者,里面包装了任务和线程对象,所以初学者过来看源码可能上来就找线程,以为ThreadPoolExecutor直接

维护了一个线程集合之类的,一下就看蒙了,不知道从何下手。

2.非核心线程

可能有些人会认为线程会根据创建顺序被标记为核心线程和非核心线程,界定标准就是在达到corePoolSize之前是核心线程,之后创建的是非核心。其实不然,Worker都是一样的,

取任务时只要满足条件都有可能被删除。

3.keepAliveTime

其实还有allowCoreThreadTimeOut这个变量,标明是否允许“核心线程”超时后回收,如果设置为true,keepAliveTime的意思就是所有线程在空闲keepAliveTime时间后都会

被释放,这里在后面的代码中会说明。

下面就通过源码来一一说明。

ThreadPoolExecutor.execute

大部分情况下,往线程池里添加任务通过java.util.concurrent.ThreadPoolExecutor.execute(Runnable)方法,当然,也有通过submit的,submit最终也是调用的execute方法,只是submit将任务包装进Future中,可以接收返回结果,通过Future的get方法实现同步等待异步线程“执行结果”(callable完成)。

我们这里就从execute方法入手吧。

线程池是有状态的,5种状态:RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED,释义如下:

线程池中肯定有一个最为关键的属性,那就是当前线程计数器,在这里:

这个变量是原子操作的,里面还蕴含着别的深意,那就是变量的高3位表示线程池状态,低29位用于计数当前线程数。

看下图就明白啦:

execute代码很简单,如下:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
View Code

老外的代码是不是很规范,注释写的很清楚,3步,具体意思看代码注释。

int c = ctl.get();

还记得ctl变量吗?就是高位保存线程池状态低位计数当前线程数的那个。

workerCountOf方法就是从刚刚那个变量中提取计数部分,即当前工作线程数。

先比较如果当前工作线程数少于核心线程数,就尝试去新增一个Worker,为什么说尝试呢,第二个参数传true,表明当前线程数和核心线程数比较,考虑到并发,addWorker里面有个自旋,会用当前线程数与核心线程数或者最大线程数比较(到底跟谁比由第二个参数决定)并CAS给ctl加1,保证只能有一个线程通过判断。

同时,addWorker也会检查线程池状态,决定是否要继续接受任务。addWorker后面详细分析。

添加Worker失败的话(可能是线程池状态发生变化或者刚好有别人占用了核心线程数导致核心线程数已满),再次获取c = ctl.get();这里要注意,代码中会有很多类似的再次获取,再次检查代码,因为这里大部分代码采用无锁

添加Worker失败,确保线程池处在running状态下尝试往队列中添加任务。

1.队列添加成功

a.int recheck = ctl.get();再次检查线程池状态,不在running的话就去队列删除任务,操作成功,reject任务。

b.如果在running状态或者不在running状态且删除失败,检查下工作线程是否为0,如果为0的话就添加一个没有任务的Worker,第一个参数为什么是null?因为代码走到这里说明任务要么从队列删除失败了(remove返回false),要么没有去队列做删除动作(isRunning返回true),所以这里选择创建一个空任务的Worker,当这个Worker启动后,会去队列中拿任务。这个逻辑后面告诉你。

第二个参数false表示为非核心线程,

看addWorker代码就知道了,false的话会拿当前线程数和最大线程数比较,如果超过的话也是不会创建线程的。

2.队列添加失败,或者不在running状态,再次尝试添加Worker处理,注意,此时第二个参数传递的是false,标明当前线程数要和最大线程数比较。

还是失败的话那就没办法了,只能reject该任务。

到这个应该主体逻辑有点轮廓了,下面详细分析核心的几个方法就会云开雾散。

重点来了,addWorker分析

由于这段代码比较长,所以换种方式来解读,直接在代码上加注释,清晰明了,注意跟上节奏。

 

// 第一个参数就是任务啦,有的调用地方传null,意思是想创建一个空任务的Worker,
// 借助Worker启动后天然地回去队列中获取任务的特点执行任务。
// 第二个参数说明该Worker是否为核心线程,是不是很难理解?其实很简单,看代码就知道,添加Worker时会去不断确认当前线程数是否满足要求,
// 这个参数就是确定当前线程数是跟核心线程数比较还是最大线程数比较。
private
boolean addWorker(Runnable firstTask, boolean core) {
     // retry是一个锚,后面会用到,因为这里采用了两个自旋嵌套,内部自旋会判断当前线程池状态是否发生变化,如果变化了,
     // continue到这里,重新外部循环获取线程池状态,并检查状态是否应该继续走内循环尝试创建Worker retry:
for (;;) {
       // 获取线程池状态
int c = ctl.get(); int rs = runStateOf(c);        // 如果当前线程池状态为SHUTDOWN或者更高,也就是不再接受任务的前提下
       // 只要以下条件之一满足就直接返回false,不去尝试创建Worker:
       // 1.res != SHUTDOWN意思就是res为STOP或者更高的话是不需要创建新Worker的,
       // 因为STOP状态下是不去执行队列中的任务并会尝试终止所有包括正在执行的任务
       // 2.firstTask != null,rs >= SHUTDOWN前提下,已经是不接受新任务了,如果还传任务进来,直接拒绝
       // 3.workQueue.isEmpty()时,代码走到这个判断时,也就是rs == SHUTDOWN的条件下,
       // SHUTDOWN状态是需要把队列中的任务处理完的,所以当队列为空时,就不去创建Worker了.
       // 一句话概括就是,SHUTDOWN以上的状态肯定是不接受新任务的,只有处于SHUTDOWN状态
       // 并且队列不为空的情况下才去创建空任务的Worker,从而去处理队列中的任务
// Check if queue empty only if necessary. if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())) return false;        // 在刚刚那个自旋里面的内部自旋,外部自旋主要是检查线程池状态觉得是否要创建Worker,这个自旋目的是CAS去将ctl增1,
       // 同时在增加前需要将当前线程数和核心线程数或者最大线程数比较,这个取决于第二个入参。
       // compareAndIncrementWorkerCount方法内容如下return ctl.compareAndSet(expect, expect + 1);如果在此过程中别人
       // 做了加1操作,这里就返回false,然后在内自旋中重新检查线程池状态,如果变化的话就跳出内循环,重新走一遍外循环,重新检查状态,
       // 这里需要注意一点的是,有可能有个线程会在状态变化后通过了compareAndIncrementWorkerCount方法,为什么会这样?
       // 因为这里没有锁,compareAndIncrementWorkerCount和下面的if (runStateOf(c) != rs)不是原子操作,为什么是一个线程?
       // 因为compareAndIncrementWorkerCount决定了同一时间只能有一个线程更新ctl成功。
       // 所以,在下面创建Worker对象的时候,在mainLock锁同步代码中会再次检查线程池状态。
for (;;) { int wc = workerCountOf(c);
          // 如果工作线程数已经达到对应的数目(核心线程数或者最大线程数),直接返回
if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false;
         // CAS操作给ctl加1:return ctl.compareAndSet(expect, expect + 1);
         // 如果加1操作成功,跳出外循环,进入下面创建Worker对象流程
if (compareAndIncrementWorkerCount(c)) break retry;
         // 走到这里,只能说这个线程实力太弱,加1操作没抢成功,再次检查下线程池状态,如果发生变化就调到外循环,重新获取并检查下线程池状态 c
= ctl.get(); // Re-read ctl if (runStateOf(c) != rs) continue retry; // else CAS failed due to workerCount change; retry inner loop } } boolean workerStarted = false; boolean workerAdded = false; Worker w = null; try {
       // 终于见到Worker创建的地方了,没完,下面还得再验证下状态,原因上面说明了。 w
= new Worker(firstTask); final Thread t = w.thread; if (t != null) {
          // 整个过程到这里才开始加锁
final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { // Recheck while holding lock. // Back out on ThreadFactory failure or if // shut down before lock acquired. int rs = runStateOf(ctl.get());             // rs < SHUTDOWN也就是RUNNING状态下或者SHUTDOWN状态下同时并没有传任务进来,说明队列中有任务要处理,
            // 这时也是符合SHUTDOWN状态的描述的,这种情况也是需要创建Worker去处理队列中的任务。
if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { if (t.isAlive()) // precheck that t is startable throw new IllegalThreadStateException();               // 将Worker加到集合中,目的是持有该Worker对象,后面interruptWorkers和interruptIdleWorkers方法会用到这个集合。
workers.add(w);
int s = workers.size();
              // 记录下largestPoolSize,含义见注释:Tracks largest attained pool size. Accessed only under mainLock.
if (s > largestPoolSize) largestPoolSize = s;
              // 经过九九八十一难,到这里,终于创建Worker成功了。 workerAdded
= true; } } finally { mainLock.unlock(); }
         // 启动Worker对象内的线程
if (workerAdded) { t.start(); workerStarted = true; } } } finally {
       // 线程启动失败,回滚操作,从workers移除并decrementWorkerCount
if (! workerStarted) addWorkerFailed(w); } return workerStarted; }

 上面的代码就是Worker创建以及线程启动过程。启动好后,线程具体做了什么呢?请看下面的分析。

Worker工作过程

Worker继承自AQS,实现Runnable接口。

上面就是Worker的构造方法,传入任务,通过线程池的ThreadFactory创建线程,并把Worker作为线程的任务。

Worker的run方法调用的线程池的runWorker,如下图所示:

 下面分析runWorker方法

runWorker

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
     // 上来就解锁?还记得构造方法里面的setState(-1)吗?通过注释就可以看出,inhibit interrupts until runWorker,这里的注释也说明了,允许interrupts w.unlock();
// allow interrupts boolean completedAbruptly = true; try {
       // 获取任务(后面详细说明getTask),获取不到的话就会结束该循环,
       // 也就是会退出线程,finally中调用processWorkerExit详细说明见后面分析
while (task != null || (task = getTask()) != null) {
         // 调用Worker的锁,Worker设计的不可重入锁就是避免线程池类似setCorePoolSize操作会中断 w.lock();
         // 确保正在STOP(runStateAtLeast)时,该线程interrupted,
         // 如果不是的话,应该清除该线程的interrupted状态,通过Thread.interrupted静态方法可以做到
         // 而实例方法interrupt只是返回是否处在interrrupted状态中,不会清除线程的状态标志
// If pool is stopping, ensure thread is interrupted; // if not, ensure thread is not interrupted. This // requires a recheck in second case to deal with // shutdownNow race while clearing interrupt if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) wt.interrupt(); try {
            // 在该类中为空实现,留给子类扩展 beforeExecute(wt, task); Throwable thrown
= null; try {
              // 真正执行任务的地方到了 task.run(); }
catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { thrown = x; throw new Error(x); } finally {
               // 空方法,留给子类扩展 afterExecute(task, thrown); } }
finally { task = null; w.completedTasks++;
            // 解锁 w.unlock(); } }
       // 走到这里表明正常退出线程 completedAbruptly
= false; } finally {
       // 处理线程退出的最后收尾工作,具体干了什么,见后面的分析 processWorkerExit(w, completedAbruptly); } }

上面的核心就是getTask从队列中获取任务,所以,我们现在来看getTask。

getTask

private Runnable getTask() {
     // 标志线程获取队列任务时是否等待超时
boolean timedOut = false; // Did the last poll() time out?      // 自旋处理,注意返回条件,一是线程池状态发生变化,二是超时情况下的逻辑,具体判断条件下面分析 for (;;) { int c = ctl.get(); int rs = runStateOf(c);        // 线程池是SHUTDOWN状态,并且队列为空(因为SHUTDOWN状态是还要把队列任务处理完的)
       // 或者是STOP状态,这两个条件下Worker数量减1并返回null,返回null后,runWorker中的while循环就会退出,线程就会结束生命
// Check if queue empty only if necessary. if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { decrementWorkerCount(); return null; } int wc = workerCountOf(c);        // 标记线程不再等待任务的前提,那就是如果设置允许少于核心线程数时启用超时,
       // 或者没有设置情况下,当前线程数大于核心线程数,就是当前线程太多了,你等了那么久还没有等到任务,你就先退下吧。
// Are workers subject to culling? boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;        // 超时未等到任务时,如果此时timed,基本你就可以结束工作,收工回家了,
       // 注意,compareAndDecrementWorkerCount通过CAS保证同时只能有一个人退出哦,避免并发情况下大家都撒手不干了,
       // 如果你幸运的留下来了,就continue再试试吧,也许你是最后一个wc == corePoolSize的幸运儿呢。
       // 为什么会有wc > maximumPoolSize判断条件呢?添加Worker的时候不是保证了不会超过maximumPoolSize吗?是不是有这个疑惑?
       // 因为线程池有setMaximumPoolSize动态调整maximumPoolSize的public方法,当缩小这个值时,会调用interruptIdleWorkers,
       // 让正在等待获取任务的线程停止等待,重走一遍循环来判断是否退出。
       // 类似的还有setCorePoolSize,allowCoreThreadTimeOut,setKeepAliveTime,shutdown

if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { if (compareAndDecrementWorkerCount(c)) return null; continue; } try {
         // 取任务时,如果是timed,也就说明这可能是你最后一次取任务了,给你等一段时间的机会,否则,一直等待,直到获取到任务。 Runnable r
= timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
         // 取到任务了,就返回吧
if (r != null) return r;
          // 歇菜了,没取到任务,此时timed && timedOut为true,那么就有可能面临收工回家的风险,
          // 在上面返回null,runWorker中退出while循环,结束线程。 timedOut
= true; } catch (InterruptedException retry) {
          // 其他地方会调用该线程的interrupt方法(比如,shutdown和shutdownNow,这两者的区别后面讲,
          // 还有上面提到的用于动态修改在这个方法中用到的变量),打断等待中的线程,这里catch后重新走一遍循环,
          // 判断当前状态是否要终止该线程的生命周期 timedOut
= false; } } }

至此,Worker创建,获取任务就分析完了,后面分析Worker的退出以及哪些情况下调用interrupt的分析。

interruptIdleWorkers

中断空闲线程,怎样区分空闲线程和正在工作的线程呢?上面提到的Worker类继承了AbstractQueuedSynchronizer,实现了简单的不可重入锁。

代码如下:

在上面的runWorker方法中,执行任务的run方法被包含在了Worker的lock中,如下所示:

所以,翻看interruptIdleWorkers代码,在调用thread.interrupt之前,调用worker.tryLock判断条件,而这个方法是立即返回的(lock方法是会等待直到获得锁)。

 

 重点来了,所谓的中断线程,也就是调用线程的实例方法interrupt,这个方法不会终止正在运行的线程,只会对阻塞状态(sleep,await,join等情况)下的线程起作用,抛出InterruptedException异常,interrupt其实是设置了一个标志位,抛出异常的同时会清除这个标志位,特别要说明的是,java.lang.Thread.interrupted()静态方法返回是否被中断标志位的同时会清除标志位信息。这个方法在runWorker的时候也有巧妙的运用,这里的知识点不明白的可以自行了解补习下,回头看,对应我们这里的场景就是getTask时,调用BlockingQueue的poll和take获取任务时处于阻塞状态下的线程。

那么,什么情况下会中断这些空闲线程呢?我们看下该方法调用的地方:

一目了然,就像上面代码注释时提到的,凡是getTask中用于判断的条件“发生变化”时(不一定是发生变化就调用,比如,setMaximumPoolSize,是有当缩小线程池的时候才会尝中断空闲线程),都会调用该方法,让那些等待的线程中断,然后再catch异常,让线程重新走一遍循环,重新判断下当前的各个条件,决定是立即返回null,还是继续等待。

interruptWorkers

 直接上代码,很简单,就是直接调用集合中所有的Worker的interruptIfStarted:

我们再看下这个方法,只要Worker启动了(state>=0,在runWorker中unlock启动),就调用线程interrupt方法。

看下调用关系,只有shutdownNow去调用,该方法顾名思义就是立即关闭线程池:

从上面的源码可以看出,分为几个步骤,先上锁-》再检查是否有shutdown权限-》再设置状态为STOP-》然后中断所有线程(当然,处于运行中状态的线程不受影响,等他们工作完了去getTask时发现STOP,就返回null退出舞台)-》获取队列中的所有未执行的任务并返回-》tryTerminate。

说到shutdownNow自然要看下shutdown方法

从上面的代码可以看出,设置的状态是SHUTDOWN,而shutdownNow是STOP状态,两者区别上面说过了,那就是SHUTDOWN会把队列中的任务执行完并且是中断空闲线程,STOP是中断所有线程(详细见上面两个interruptWorkers和interruptIdleWorkers的分析),不过他也不是不负责任的人,会把队列中的所有未执行的任务返回。在shutdown方法中还会调用onShutdown方法,这个方法也是供子类实现的。

 

上面的过程中,涉及线程池的创建,线程(Worker)的创建,销毁等,在线程池的终止过程中,还有个重要的角色,那就是tryTerminate方法,下面来看看他吧。

tryTerminate

代码如下:

又是一个自旋,结合的是ctl.compareAndSet这个操作。

循环体内,首先判断状态,如果是RUNNING或者已经在TIDYING状态之后了,又或者是SHUTDOWN状态,但是队列不为空,这三种情况下直接返回。

其次,如果工作线程数不为0,调用interruptIdleWorkers同时传入true,中断一个空闲线程(中断过程见上面的分析)。然后直接返回。你可能会想,怎么中断一个空闲线程就返回了?那还怎么停掉整个线程池?稍安勿躁,稍后为你揭晓。如果上面都通过了,也就是状态在SHUTDOWN并且队列为空,或者状态在STOP状态,同时此时工作线程数为0,进入下面代码:

加锁,设置状态为TIDYING,调用子类需要实现的terminated方法,然后设置状态为TERMINATED,最后termination.signalAll()唤醒awaitTermination中对应的lock创建出来的同一个condition (termination.awaitNanos),知识点又来了,AQS的condition可以自行去了解下。

解惑,上面提到的tryTerminate只会中断一个空闲线程,那如何终止整个线程池呢?

看见下图就明白了:

明白了吗?如果还没明白,说明你没有跟上节奏,对线程池的代码还没吃透,重新看读几遍本文吧。哈哈。。。

给你提个醒,processWorkerExit这个方法调用了tryTerminate,而每一个Worker的退出都要走这个方法,所以,明白了吧,每退出一个Worker都会去尝试终止线程池,所以方法名称为什么叫tryTerminate,尝试终止。纵观调用的地方,其实在所有可能导致SHUTDOWN状态的地方都去调用了tryTerminate。

 

最后,上面提到的awaitTermination方法在写线程池停止代码也很重要,用于等待获取线程池的彻底终止状态。

代码如下:

核心也是上面提到的termination.awaitNanos,AQS中的condition,这个知识不在本文返回,后面会分析AQS代码,有兴趣的可以关注下。

RejectedExecutionHandler

内部类提供了几种策略,如下:

CallerRunsPolicy,交给调用者线程run:

AbortPolicy,直接中断,以异常的形式反馈出去:

DiscardPolicy,直接忽略,不做任何处理:

DiscardOldestPolicy,忽略最老的任务,执行当前任务:

默认策略是AbortPolicy:

 

posted on 2019-04-25 18:11  砌码匠人  阅读(233)  评论(0编辑  收藏  举报

导航