从源码分析线程池ThreadPoolExecutor的实现原理

从源码分析线程池ThreadPoolExecutor的实现原理

前提

闲来无事,研究了项目里的一些线程池的配置,就顺便了解一下ThreadPoolExecutor的内部的执行逻辑,浅浅的分析一下源码,最后再整理出这篇文章,当做记录一下。这篇文章的源码是基于JDK17

ThreadPoolExecutor大家都知道,它是juc包底下的一个管理线程池的类,是Executor框架的一个实现,以及它有各种各样的优点...这里就不再过赘述。

原理

线程池创建

我们从线程池的创建开始,一步步深入研究它的一个执行的逻辑。线程池的创建主要有两大类方式,一是通过ThreadPoolExecutor构造函数创建,二是通过Executors类创建。

  • ThreadPoolExecutor构造函数创建
csharp 复制代码
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

线程池的七大参数,这里就稍微带过一下:

corePoolSize :核心线程数量。 maximumPoolSize :最大线程数量。 keepAliveTime :线程空闲等待时间,也和工作线程的生命周期有关,下文会分析。 unit :TimeUnit类型,keepAliveTime参数的时间单位,实际上keepAliveTime最终会转化为纳秒。 workQueue :阻塞队列,等待队列或者叫任务队列。 threadFactory :线程工厂,默认使用Executors.defaultThreadFactory()作为内建线程工厂实例 handler:线程池的拒绝策略。提供了4种内建的拒绝策略实现:AbortPolicy、DiscardPolicy、DiscardOldestPolicy、CallerRunsPolicy

  • Executors创建

    可以点到该类里面去看一下,这里面有很多创建线程池的方法,这里不一一介绍了。

    csharp 复制代码
    public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
    }
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>()));
    }

线程池执行

当我们创建线程池后,调用线程池的excute(或者submit方法,有一点区别)

typescript 复制代码
 public static void main(String[] args) {
       ThreadPoolExecutor pool =new ThreadPoolExecutor(
                0,
                2,
                0,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(1000)
        );
        pool.execute(()->{
            System.out.println("thread测试");
        });
    }

准备工作

在分析excute之前,我们先看下ThreadPoolExecutor类中几个关键的成员变量

java 复制代码
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
​
// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;
​
// Packing and unpacking ctl
private static int runStateOf(int c)     { return c & ~COUNT_MASK; }
private static int workerCountOf(int c)  { return c & COUNT_MASK; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
//全局锁
private final ReentrantLock mainLock = new ReentrantLock();
//Worker集合
private final HashSet<Worker> workers = new HashSet<>();
  • ctlAtomicInteger类型字段,状态控制成员变量,主要是用来存储两个概念的字段:当前线程池中的工作线程数 以及当前线程的状态

    整型包装类型Integer占4个字节,也就是一共32位

    线程池一共有5种状态,所以2^3 = 8,使用3位bit就能表示线程的状态了,所以剩余的29位bit就是用来表示工作线程数了

  • COUNT_BITS :int类型,Integer.SIZE - 3显而易见是29

  • COUNT_MASK :int类型,这个可以理解为线程最大数量,在JDK8中对应的是CAPACITY,也就是容量

    这里用到了<<左移运算,将1向左移动29位,也就是001 000...000【1后面29个0】,也就是1^29,那么再减1得到的结果就是

    000 111.....111【一共29个1】,我们这里全部给它补齐为32位

接下来这五个字段,是用来表示线程状态的常量:

  • RUNNING

    在RUNNING状态下,线程池可以接收新的任务和执行已添加的任务。

    -1左移29位,由于负数在计算机中都是以补码的形式存储,所以我们得先计算出-1的补码

    -1源码:1000....0001【最高位1表示负数】

    -1反码:1111....1110【符号位不变,剩下的位取反操作】

    -1的补码:1111....1111【反码+1】

    -1的补码左移29位:111....0000【29个0】

  • SHUTDOWN

    线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。当一个线程池调用shutdown()方法时,线程池由RUNNING -> SHUTDOWN。

    0左移29位:0000....0000【32个0】

  • STOP

    线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在执行的任务。调用线程池的shutdownNow()方法的时候,线程池由(RUNNING或者SHUTDOWN ) -> STOP。

    1左移29位:001000....0000【连续29个0】

  • TIDYING

    当所有的任务已终止,记录的"任务数量"为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,会由STOP -> TIDYING。

    2左移29位:010 000....000【连续30个0】

  • TERMINATED

    当钩子函数terminated()被执行完成之后,线程池彻底终止,就变成TERMINATED状态。线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。

    3左移29位:011 000....000【连续29个0】

我们再看一下runStateOf方法,该方法就是用来获取线程池的状态,并且可以同理得出workerCountOf方法就是获取低29位的工作线程数了。

COUNT_MASK上面分析过了,是000 111.....111【一共29个1】,取反~后是111 000...000【29个0】,再与&上c

由于后29位都0,所以低29位都是0,只比较高三位,与高三位111相与都为本身,即当前线程池的状态

最后我们再来看下ctlOf这个方法,该方法两个入参rs为运行状态,wc为工作线程数。我们刚才说过,ctl用来表示线程池的运行状态和工作线程数,那如果rs的状态为RUNNING,工作线程数wc为5,怎么使用ctl来表示呢?

rs【状态RUNNING】111000...000【29个0】

wc【工作线程数5】 000000....101【连续29个0】

rs | wc 得到的结果就是111000....0101,可以看到高3位就是当前线程池状态111也就是RUNNING

低29位就是当前的工作线程数000...0101也就是5,通过使用一个int就能同时表示两个业务变量

至此,分析execute前的准备工作已经完成

execute方法分析

接下来我们到execute方法来,逐一分析它的执行逻辑

scss 复制代码
public void execute(Runnable command) {
    //这里就是一个代码健壮性的判断
    if (command == null)
        throw new NullPointerException()
    //获取当前ctl的值
    int c = ctl.get();
    //刚刚分析过workerCountOf,取到当前工作线程的数量
    //如果当前工作的线程数小于核心线程,则可以直接添加工作也就是addWorker方法
    if (workerCountOf(c) < corePoolSize) {
      //如果创建工作线程成功就直接返回了
        if (addWorker(command, true))
            return;
      //否则这里会重新获取一下ctl的值
        c = ctl.get();
    }
    //如果走到这里说明workerCountOf>核心线程数,或者是addWorker失败了
    //判断线程池是否处于运行中状态,同时尝试用非阻塞方法向任务队列放入任务
    if (isRunning(c) && workQueue.offer(command)) {
        //投放任务成功,对线程池的运行中状态做二次检查
        int recheck = ctl.get();
        // 如果线程池二次检查状态是非运行中状态
        // 则从任务队列移除当前的任务(也就是移除前面成功入队的任务实例)
        if (! isRunning(recheck) && remove(command))
            //调用拒绝策略处理
            reject(command);
        //如果走到了else if 说明有以下的前提:
        //1.workQueue.offer成功(任务成功加入队列)
        //2.线程池可能是running状态
        //3.remove移除任务时返回false
        //如果当前的工作线程数为0,则添加一个空任务,并且为非核心线程,结束
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
        //最后的else分支应该是工作线程数量不为0,这边什么也不做,因为任务已经成功入队列
        //后续会有合适的时机分配其他空闲线程去执行它 后面getTask方法会说到
    }
    // 走到这里说明有以下的前提:
    // 1、线程池中的工作线程总数已经大于等于corePoolSize核心线程数
    // 2、线程池可能不是RUNNING状态
    // 3、线程池可能是RUNNING状态同时任务队列已经满了,这时候会尝试创建非核心线程传入任务执行
    // 创建非核心线程失败,此时需要拒绝执行任务
    else if (!addWorker(command, false))
        reject(command);
}

这里总结下execute的大概流程以及疑惑点

  1. 如果当前的工作线程数小于核心线程数corePoolSize,会直接调用addWork去创建核心线程,为什么可以知道是核心线程呢,因为addWork方法的第二个参数就是判断是否核心线程。
java 复制代码
private boolean addWorker(Runnable firstTask, boolean core)
  1. 如果当前工作线程总数大于等于核心线程数corePoolSize,判断线程池是否处于运行中状态,同时尝试用非阻塞方法向任务队列放入任务

  2. 二次检查线程池运行状态,如果此时线程池是非运行中状态,则调用remove方法移除该任务,如果移除成功了,则执行拒绝策略,如果失败,则说明任务可能已经被执行了

    ! isRunning(recheck),这里如果线程池是非running状态的,会直接执行remove(command)方法,但是我看资料有看到说,shutdown状态可以处理队列中的已有任务,那么这里为什么要把任务直接remove掉呢?

    我个人的理解是(如有错误感谢指出):

    1.shutdown状态可以处理队列中的已有任务,这里的说的可以处理队列中的已有任务其实是针对tryTerminate()方法的,这个方法中有个判断,runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty(),如果是shutdown状态,并且是队列不为空,那么该方法会直接返回,也就是不会将线程池的状态变更为TIDYING以及后续的TERMINATED

    2.在线程池关闭的时候我们通常会调用shutdown,然后再调用awaitTermination方法,设置一个时间上限,如果超出了时间,会执行shutdownNow方法,将线程池的状态设置为STOP。假设不执行remove方法将任务移除掉,并且任务被正常执行,那么如果超过了我们设置的时间,该任务也会因为线程池状态变为STOP而被强制中断。所以此处直接remove掉,执行拒绝策略更为稳妥。

    scss 复制代码
    //这是实际项目中关闭线程池的方法
    static {
     Runtime.getRuntime().addShutdownHook(new Thread(() -> {
         log.info("关闭线程池senderExecutor");
         senderExecutor.shutdown();
         try {
             if (!senderExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
                 senderExecutor.shutdownNow();
             }
         } catch (InterruptedException ie) {
             senderExecutor.shutdownNow();
             Thread.currentThread().interrupt();
         }
         log.info("线程池senderExecutor关闭成功");
     }));
    }
  3. 如果线程池是running 或者 任务移除失败,查询工作线程数,如果为0,则添加一个非核心线程空任务

    此处工作线程为什么要addWork(null,false)呢?

    存在这种情况:当线程池中没有工作线程的时候,但是阻塞队列又有任务的时候,如果不新建一个工作线程去处理任务,只有等待下一次阻塞队列添加任务的时候,这时候第一个任务才会被执行,这种情况会造成任务被延迟执行。

    情景1:当核心线程数设置为0的时候

    scss 复制代码
    public void execute(Runnable command) {
    if (command == null)
      throw new NullPointerException();
    int c = ctl.get();
    //此处判断被跳过
    if (workerCountOf(c) < corePoolSize) {
      if (addWorker(command, true))
          return;
      c = ctl.get();
    }
    //此时任务会被成功放入阻塞队列workQueue
    if (isRunning(c) && workQueue.offer(command)) {
      int recheck = ctl.get();
      if (! isRunning(recheck) && remove(command))
          reject(command);
    //如果此处没有addWork 则该任务不会执行,直到阻塞队列workQueue达到容量上限
      else if (workerCountOf(recheck) == 0)
          addWorker(null, false);
    }
    //达到容量上限后,workQueue.offer调用失败,此时才会开始添加一个工作线程(非核心线程)
    else if (!addWorker(command, false))
      reject(command);
    }

    情景2:当核心线程数设置非0,但是线程池的allowCoreThreadTimeOut该参数为true,此时,核心线程也会根据keepAliveTime时间来销毁线程。

    scss 复制代码
    public void execute(Runnable command) {
     if (command == null)
         throw new NullPointerException();
     int c = ctl.get();
    //<1>处
    //假设此时工作线程为5,核心线程为5,跳过判断
     if (workerCountOf(c) < corePoolSize) {
         if (addWorker(command, true))
             return;
         c = ctl.get();
     }
    //假设此时核心线程已经执行完任务,并且达到 keepAliveTime,5个核心线程被销毁
    //但是此时 任务又被成功放入阻塞队列workQueue
     if (isRunning(c) && workQueue.offer(command)) {
         int recheck = ctl.get();
         if (! isRunning(recheck) && remove(command))
             reject(command);
       //如果此处没有addWork 则该任务不会执行,直到下一个任务进来,调用execute方法时,才会在<1>处被执行
       //此时可能已经过了很长时间了
         else if (workerCountOf(recheck) == 0)
             addWorker(null, false);
     }
    //达到容量上限后,workQueue.offer调用失败,此时才会开始添加一个工作线程(非核心线程)
     else if (!addWorker(command, false))
         reject(command);
    }
  4. 走到最后的else if,如果创建工作线程失败,则执行拒绝策略。可以从判断得知,如果线程池不是RUNNING状态(假设是STOP),也会调用addWorker()方法,按理说该状态不应该再添加任务,但是其实在addWork()中又会再次判断线程池状态,后面会详解该方法。

addWork方法分析

execute整体的流程我们了解了,现在再看看addWork方法,看看到底做了什么,这里我们分成两个部分去分析,第一部分是看下retry这个循环做了什么,第二部分就是执行线程的start方法

kotlin 复制代码
private boolean addWorker(Runnable firstTask, boolean core) {
    //循环标志
    retry:
    //外层循环-死循环
    for (int c = ctl.get();;) {
        //拆解下这里的条件判断
        //条件1.runStateAtLeast(c, SHUTDOWN) 判断当前线程池状态是否>=shutdown状态
        //在条件1的情况下,如果以下条件2中只要有一个成立,就会直接返回false
        //条件2.1 runStateAtLeast 判断当前线程池状态是否>=STOP状态
        //条件2.2 firstTask!=NULL
        //条件2.3 workQueue队列为空
        //其实这个判断就是如果线程池状态为shutdown、stop、tidying、trminated
        //满足上述状态下,如果状态已经到了STOP、或者传入任务不为空、或者任务队列为空都不需要添加新的线程
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP)
                || firstTask != null
                || workQueue.isEmpty()))
            return false;
        //内存循环-死循环
        for (;;) {
            //先看core ? corePoolSize : maximumPoolSize 判断添加的是否核心线程
            //core参数是在addWork时传入的
            //是-返回配置的核心线程数  否-返回配置的最大线程数
            //这里就是判断当前的工作线程数是否已经大于等于核心线程数或者最大线程数
            //是的话返回return false,创建工作线程失败
            if (workerCountOf(c)
                >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                return false;
            //这里就是进行CAS操作  将工作线程的数量+1
            //因为这里CAS操作成功了,就会跳出循环,去执行第二部分线程启动start方法,说明要开始执行任务了
            if (compareAndIncrementWorkerCount(c))
                //这里break retry; 就是直接结束最外层循环了
                break retry;
            //执行到这里,就说明CAS操作失败了,重新取出ctl的值
            c = ctl.get();
            //重新判断线程池的状态 如果大于>=SHUTDOWN 则跳到外层循环,继续循环执行
            if (runStateAtLeast(c, SHUTDOWN))
                //跳到外层循环,继续循环执行
                continue retry;
           // else CAS failed due to workerCount change; retry inner loop 
            //如果线程池状态依然是RUNNING,CAS更新工作线程数失败说明有可能是并发更新导致的失败,则在内层循环重试即可
        }
    }
    .......
}

这里的firstTask!=null 主要是为了addWork(null,false)这种参数的添加工作线程的方法也能被顺利添加,也就是对应我们execute方法里说的,用于处理队列有任务,而工作线程为0的情况。

kotlin 复制代码
if (runStateAtLeast(c, SHUTDOWN)
 && (runStateAtLeast(c, STOP)
     || firstTask != null
     || workQueue.isEmpty()))
 return false;

接着第二部分

ini 复制代码
private boolean addWorker(Runnable firstTask, boolean core) {
    .......
    //状态标志  工作线程是否启动
    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 {
                int c = ctl.get();
                //如果线程池是running状态 则需要添加到工作集合workers中
                //如果至少是STOP状态,并且firstTask为null,也添加到工作集合workers中
                if (isRunning(c) ||
                    (runStateLessThan(c, STOP) && firstTask == null)) {
                    //如果线程的状态不是new,抛出异常
                    if (t.getState() != Thread.State.NEW)
                        throw new IllegalThreadStateException();
                    //否则添加到workers集合
                    workers.add(w);
                    //添加标记变为true  成功
                    workerAdded = true;
                    //处理更新一下历史工作线程的最大数
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                }
            } finally {
                mainLock.unlock();
            }
            //如果添加到集合成功,这里开始启动线程
            //Thread#start()方法启动真实的线程实例
            if (workerAdded) {
                t.start();
                //线程启动标志 更为为true
                workerStarted = true;
            }
        }
    } finally {
      // 线程启动失败,需要从工作线程集合移除对应的Worker
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

有几个点再详细解释下

  • 全局锁。我们可以看到,这里有个workers.add(w);调用,这个workers是个HashSet集合,它的add方法是线程不安全的;其次,为了防止当任务准备执行之时,线程池被STOP等。

    swift 复制代码
    private final HashSet<Worker> workers = new HashSet<>();
  • firstTask的空任务判断。这里的判断也是上面说到的addWork(null,false)新增空任务工作线程

    ini 复制代码
    if (isRunning(c) ||
                        (runStateLessThan(c, STOP) && firstTask == null))

Worker类分析

我们可以看到,这里new了一个Worker类,并且在任务firstTask传入

ini 复制代码
Worker w = null;
try {
    w = new Worker(firstTask);
    final Thread t = w.thread;
  ....
}

可以看到,Worker继承了AQS,并且实现了Runnable接口,并继承了AbstractQueuedSynchronizer(AQS),并且持有一个Thread实例,以及任务实例firstTask

typescript 复制代码
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
  final Thread thread;
  Runnable firstTask;
  Worker(Runnable firstTask) {
      //此处设置为-1是防止线程被中
      //当调用shutDownNow方法时会将state大于等于0的工作队列中断
      setState(-1); 
      //传入的任务实例
      this.firstTask = firstTask;
      //通过ThreadFactory创建线程实例
      //Worker实例自身作为Runnable用于创建新的线程实例
      this.thread = getThreadFactory().newThread(this);
  }
  
  //重写Runnable的run方法,实际是委托外部runWorker方法执行
  public void run() {
      runWorker(this);
  }
  
  //下面3个都是重写自AQS的方法
  
  // The value 0 represents the unlocked state.
  // The value 1 represents the locked state.
  //是否持有独占锁,state值为1的时候表示持有锁,state值为0的时候表示已经释放锁
  protected boolean isHeldExclusively() {
      return getState() != 0;
  }
​
  //尝试获取锁的方法,AQS的钩子函数 具体由每个不同类型的锁去实现该方法,如ReentrantLock
  //此处是Worker类自己的实现
  protected boolean tryAcquire(int unused) {
      if (compareAndSetState(0, 1)) {
          setExclusiveOwnerThread(Thread.currentThread());
          return true;
      }
      return false;
  }
  //尝试释放锁  也是钩子函数 同tryAcquire
  protected boolean tryRelease(int unused) {
      setExclusiveOwnerThread(null);
      setState(0);
      return true;
   }
}
​
//acquire,release都是AQS中的方法
//加锁
public void lock()        { acquire(1); }
//尝试加锁
public boolean tryLock()  { return tryAcquire(1); }
//解锁
public void unlock()      { release(1); }
//是否已被加锁
public boolean isLocked() { return isHeldExclusively(); }
//进行线程中断
void interruptIfStarted() {
    Thread t;
    if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
        try {
            t.interrupt();
        } catch (SecurityException ignore) {
        }
    }
}
  

addWorker方法中,调用了t.start();方法,从上面的分析可以看出,t.start();调用的是Worker中的run方法,又委托外部runWorker方法执行。接着我们就重点看下runWorker

scss 复制代码
final void runWorker(Worker w) {
    //获取当前线程   实际上和Worker持有的线程实例是相同的
    Thread wt = Thread.currentThread(); 
    //获取woker初始化时候 传入的任务对象  存放到变量task中
    Runnable task = w.firstTask;
    w.firstTask = null;
    //释放锁,设置AQS中的state状态为0,允许打算,原先在Worker构造中的值为-1
    w.unlock(); // allow interrupts
    //
    boolean completedAbruptly = true;
    try {
        //循环,如果worker初始化时传进来的任务不为null,或者 getTask()返回的任务不为null
        //getTask()后面会讲到,先理解它是一个阻塞队列,获取队列中的任务
        while (task != null || (task = getTask()) != null) {
            //加锁,本质是AQS
            w.lock();
            // 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
            //如果池正在停止,请确保线程被中断;
            //如果没有,请确保线程不被中断。
            //这需要在第二种情况下重新检查,为了处理shutdownNow线程中断时无异常
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                //钩子方法 任务执行前
                beforeExecute(wt, task);
                try {
                    //任务目标的run方法,也就是我们自己的任务代码
                    task.run();
                    //钩子方法 任务之前后 正常情况
                    afterExecute(task, null);
                } catch (Throwable ex) {
                    //钩子方法 任务之前后 异常情况
                    afterExecute(task, ex);
                    throw ex;
                }
            } finally {
                //清空任务  以便while循环继续获取新任务
                task = null;
                //累加完成的任务数
                w.completedTasks++;
                //释放锁
                w.unlock();
            }
        }
        //getTask为null,线程正常退出
        completedAbruptly = false;
    } finally {
        //处理线程退出
        //根据completedAbruptly的值   会有一些不同的处理
        //正常退出completedAbruptly为false,否则为true
        processWorkerExit(w, completedAbruptly);
    }
}

这里我们重点分析下线程中断的判断

  • 线程中断判断
less 复制代码
if ((runStateAtLeast(ctl.get(), STOP) ||
  (Thread.interrupted() &&
   runStateAtLeast(ctl.get(), STOP))) &&
 !wt.isInterrupted())
 wt.interrupt();

我们拆分一下这个判断

boolean A = runStateAtLeast(ctl.get(), STOP) 判断线程池状态是否至少为STOP,也就是rs>=STOP

boolean B = Thread.interrupted() && runStateAtLeast(ctl.get(), STOP)判断当前线程的中断状态,并且线程池状态是否至少为STOP

boolean C = !wt.isInterrupted()当前线程是否已中断

结果就是:

css 复制代码
if ( (A || B) && C){
    wt.interrupt();
}

正如英文解释的一样:

  • 如果线程池处于STOP以上的状态,那么就一定要保证当前线程是中断的状态, 也就是说如果A为true,那么如果当前线程不是中断状态!wt.isInterrupted(),就要调用wt.interrupt()中断当前线程
  • 如果A为false,假设线程池的状态是RUNNING,判断B,会执行Thread.interrupted()方法,返回当前线程是否中断的状态(同时也会重置当前线程的中断状态为false ),如果Thread.interrupted()返回false,则说明当前线程没有被中断,则不会再继续判断后续的runStateAtLeast(ctl.get(), STOP)),则(A||B)为false,则整个表达式为false,不会调用!wt.isInterrupted()方法;那如果Thread.interrupted()返回true,说明当前线程被中断了,那么会继续判断当前线程池状态是否>=STOP,如果不是>=STOP,那么整个表达式也是返回false;那如果线程池是>=STOP,说明线程池正在停止,那么就一定要保证当前线程被中断,也就是B返回true,由于Thread.interrupted()重置了线程的状态为false,所以C返回的也为true,整个表达式就是true,执行线程中断方法。
  • 还有个点就是,为什么会再一次判断runStateAtLeast(ctl.get(), STOP)方法,因为有可能进入该判断逻辑时,外部线程有可能调用shutdownNow()方法。

getTask方法分析

java 复制代码
private Runnable getTask() {
    //记录上一次从任务队列中拉取任务 是否超时
    boolean timedOut = false; // Did the last poll() time out?
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        //条件1:线程池非RUNNING状态
        //条件2.1线程池是大于STOP的状态 条件2.2 工作队列为空
        //在条件1成立的情况下,如果条件2.1满足,则会调用decrementWorkerCount();并且返回null
        //在条件1成立的情况下,如果条件2.1不满足,则会判断2.2如果满足则会调用decrementWorkerCount();并且返回null
        //这里也同时印证了我们上面说的线程池状态:SHUTDOWN状态会继续处理工作队列的任务,如果队列不为空的话
        //STOP状态则会直接不处理任何任务
        //这里其实就是判断 什么时候要把该工作该工作线程废弃
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            //工作线程数减1
            //这里只是单纯将工作线程数量减1,并没有将该worker移除workers集合
            //会在后面的processWorkerExit方法中移除
            decrementWorkerCount();
            //返回null,getTask返回null,上一层的任务循环就会正常结束
            return null;
        }
        //当前线程工作数
        int wc = workerCountOf(c);
​
        //allowCoreThreadTimeOut是否允许核心线程超时,默认为false
        //wc > corePoolSize 当前线程是否大于核心线程数
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        //这里住要判断该工作线程是否需要剔除掉(也可以理解为废弃)
        //会在下面具体分析
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            //如果是要剔除的话 将工作线程数量减1
            //同上面一样   会在后面的processWorkerExit方法中移除
            if (compareAndDecrementWorkerCount(c))
                //返回null
                return null;
            //如果CAS失败了  说明有其他线程-1成功了,重新循环
            continue;
        }
​
        try {
            //这里就是从队列取任务
            //会在下面具体分析
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            //获取到的null 则返回该任务
            if (r != null)
                return r;
            //否则则说明超时了
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

这里我们先分析下获取任务的逻辑

ini 复制代码
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
  • timed如果为true,则表示允许核心线程超时,或者当前线程数大于核心线程数,则workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)此处的keepAliveTime就是线程池构造方法的线程空闲等待时间 ),该方法会从队列中取任务,并且有超时时间keepAliveTime,如果取到了任务,则直接返回该任务,否则说明poll()方法超时了,此时设置timedOut为true
  • timed如果为false,则说明不允许核心线程超时,并且当前工作线程小于核心线程数,则workQueue.take(),该方法是阻塞方法,直到获取任务或者被其他线程打断,然后进入到catch方法,将timedOut设置为false,重新循环。

ps:回想下上面说的addWorker(Runnable firstTask, boolean core),core字段表示当前的工作线程是否为核心线程,但是执行到这里boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;的时候,其实判断该线程是否为核心线程就靠wc > corePoolSize;,如果当前线程小于核心线程数,那么我就认为当前的工作线程是核心线程,然后根据allowCoreThreadTimeOut是否允许核心线程超时,来判断调用的poll()还是take()。当然这里会先判断allowCoreThreadTimeOut是否允许核心线程超时,如果允许,不管你是核心线程还是非核心线程,都有超时时间。

接着看一下这if判断的逻辑

bash 复制代码
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
         && (wc > 1 || workQueue.isEmpty())) {
         if (compareAndDecrementWorkerCount(c))
             return null;
         continue;
}

这里我们拆分一下:

boolean A = wc > maximumPoolSize

boolean B = timed && timedOut

boolean C = wc > 1

boolean D = workQueue.isEmpty()

css 复制代码
if ( (A || B) && ( C || D ) ){
    .....
}

可以看到,要想干掉当前工作线程 ,必须满足(A || B) 和 ( C || D )同时为true,我们先看下C和D,wc > 1 如果线程池还有其他的 工作线程,那么就可以干掉当前线程(保证至少还有一个线程来处理任务队列);如果wc>1不满足,判断工作队列,如果为空,说明没有待处理的任务,也可以干掉当前线程。C和D只要一个为true即可。

那么在C||D返回true的大前提下,分析下A和B什么情况下能干掉当前线程,wc > maximumPoolSize 如果当前工作线程大于最大线程数(setMaximumPoolSize()方法 可能外部线程将线程池最大的线程数调小),则表示可以干掉当前线程。如果wc > maximumPoolSize不满足,判断 timed && timedOut,可以看到timedOut当第一次循环的时候是false,所以第一次循环的时候,B一定为false,也就是只有A为true的是,第一循环才会干掉该线程,否则则会往下走,去获取任务。

那么如果第一次循环成功获取了任务,就会直接返回该任务,那我们分析下,如果获取任务超时,则timedOut为true,第二次循环时,同样还是在C||D返回true的大前提下,此时同样判断wc > maximumPoolSize,假设它为false,看timed && timedOut,关键看timed,如果为true,就可以干掉当前线程。而timed也在上面分析过了,如果允许核心线程超时,或者不允许核心线程超时并且工作线程数大于核心线程数,那么A||B就为true,就会进入if内的方法调用。

总结:在大多数情况,我们会将allowCoreThreadTimeOut设置为false,默认也是false,并且wc > maximumPoolSize大多数情况下也为false,所以这个判断可以总结为:如果线程获取任务已经超时了并且当前线程为非核心线程,同时工作队列为空或者还有其他的工作线程,那么该工作线程就可以被干掉

processWorkerExit方法分析

在上面,我们分析了getTask()中会将工作线程数-1,之后需要在集合中移除,那么就是在该方法中进行的

scss 复制代码
private void processWorkerExit(Worker w, boolean completedAbruptly) {
    //completedAbruptly分析过 如果为true 说明是异常退出
    //那么这里需要将工作线程数-1
    if (completedAbruptly)
        decrementWorkerCount();
    //加锁
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        //全局的已完成任务记录数加上此将要终结的Worker中的已完成任务数
        completedTaskCount += w.completedTasks;
        //工作线程集合中移除此work
        workers.remove(w);
    } finally {
        //解锁
        mainLock.unlock();
    }
  
    //尝试终止线程池 
    //该方法具体下面会分析
    tryTerminate();
  
    int c = ctl.get();
    //如果当前线程池是RUNNING或者SHUTDOWNh状态
    if (runStateLessThan(c, STOP)) {
        //如果工作线程是正常退出
        if (!completedAbruptly) {
            //如果核心线程允许超时,那么最小工作线程的数量就是0,否则最小的工作线程数量就是核心线程数
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            //如果最小的工作线程数为0  但是工作队列又不是为空
            if (min == 0 && ! workQueue.isEmpty())
                //那么就讲最小工作线程数设置为1  也就是说保证有一个线程能处理工作队列中的任务
                min = 1;
            //如果当前线程中的工作线程数 大于等于这个最小值那么不处理直接retur
            //否则 就会走到addWorker(null, false);方法 添加一个非核心工作线程,保证队列任务有线程去处理
            if (workerCountOf(c) >= min)
                return; // replacement not needed
        }
        //所以这里有两种情况添加非核心工作线程 
        //1.工作线程异常退出completedAbruptly为false
        //2.当前状态为RUNNING 或SHUTDOWN,并且当前任务队列有任务,并且当前线程数量<corePoolSize
        addWorker(null, false);
    }
}

不知道大家在这里会不会有个疑惑,这里判断的是工作线程小于核心线程数,那为什么addWorker却是添加一个非核心工作线程呢?其实在分析getTask()的时候解释过,在getTask()已经不需要true和false去区分是否核心线程了,当执行到getTask()boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;时候,只要wc<=corePoolSize,就可以认为当前工作线程是核心线程。所以addWorker(null, false);可能无关紧要了

tryTerminate方法分析

scss 复制代码
final void tryTerminate() {
    for (;;) {
        int c = ctl.get();
        //1.如果线程池还是running状态  说明还在运行 直接返回
        //2.如果线程池状态是TIDYING 或者 TERMINATED 说明线程池已经执行了 tryTerminate(),并且成功terminate,现在的线程           不应该再terminate
        //3.如果线程池是RUNNING SHUTDOWN状态 并且工作队列还有任务 那还需要继续将队列中的任务完成
        //满足以上3个条件之1  说明线程池还不能终止
        if (isRunning(c) ||
            runStateAtLeast(c, TIDYING) ||
            (runStateLessThan(c, STOP) && ! workQueue.isEmpty()))
            //直接结束
            return;
        //否则  判断工作线程数 如果不等于0
        if (workerCountOf(c) != 0) { // Eligible to terminate
            //只中断一个空闲线程 ONLY_ONE = TRUE
            interruptIdleWorkers(ONLY_ONE);
            return;
        }
​
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            //CAS设置线程池状态为TIDYING
            if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                try {
                    //如果CAS成功则执行钩子方法terminated()
                    terminated();
                } finally {
                    //最后更新线程池状态为TERMINATED
                    ctl.set(ctlOf(TERMINATED, 0));
                    // // 唤醒阻塞在termination条件的所有线程
                    //这个变量的await()方法在awaitTermination()中调用
                    termination.signalAll();
                }
                return;
            }
        } finally {
            mainLock.unlock();
        }
        // else retry on failed CAS
    }
}

分析下中断线程的方法

scss 复制代码
if (workerCountOf(c) != 0) { // Eligible to terminate
      interruptIdleWorkers(ONLY_ONE);
      return;
}

这里为什么只中断一个线程呢?根据我所查询的资料:当线程池满足终止条件但仍有空闲线程存在时,为了确保线程池能够尽快终止,tryTerminate() 方法会中断一个空闲线程,通过中断空闲线程,可以唤醒该线程并使其从等待状态返回,进而检查线程池的终止条件,这样做的目的是确保终止信号能够在线程池中传播,以便线程池能够及时终止并释放资源。通过中断空闲线程,可以避免线程池因为存在空闲线程而无法终止的情况。

可以先稍微看下interruptIdleWorkers方法。他会去遍历workers集合,如果该Worker工作线程没有被打算,那么就会去尝试获取锁,如果该线程正在执行任务,那么我们上面分析过,runWork的时候是会去加锁的w.lock();,如果此处tryLock获取成功,说明该线程没有在执行任务,属于空闲线程,那么就会调用t.interrupt();方法打断。那么又怎么理解终止信号能够在线程池中传播呢?我们在上面的getTask()方法中分析过,如果该线程是核心线程并且不允许超时,那么会调用的workQueue.take();方法阻塞,直到获取任务,那么如果不去打断该线程,那么该线程就无法被终止。

java 复制代码
private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}

shutdown/shutdownNow方法分析

在实际项目开发中,当项目终止时,jvm关闭时,我们要关闭整个线程池,那么此时就会调用线程池的shutDown以及shutdownNow方法去关闭线程池

less 复制代码
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    log.info("关闭线程池senderExecutor");
    senderExecutor.shutdown();
    try {
        if (!senderExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
            senderExecutor.shutdownNow();
        }
    } catch (InterruptedException ie) {
        senderExecutor.shutdownNow();
        Thread.currentThread().interrupt();
    }
    log.info("线程池senderExecutor关闭成功");
}));

先看下shutdown方法

scss 复制代码
public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    //加锁
    mainLock.lock();
    try {
        //权限校验,安全策略相关判断
        checkShutdownAccess();
        //更新为SHUTDOWN状态
        advanceRunState(SHUTDOWN);
        //中断所有线程
        interruptIdleWorkers();
        //钩子方法
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    //线程池shutdown后 会尝试继续将线程池变为 TIDYING  然后  TERMINATED
    tryTerminate();
}

advanceRunState方法:

csharp 复制代码
private void advanceRunState(int targetState) {
    // assert targetState == SHUTDOWN || targetState == STOP;
    for (;;) {
        int c = ctl.get();
        //如果线程池已经是targetState状态,在shutdown中也及时SHUTDOWN状态
        //或者CAS将线程池状态更新成功  那么就结束循环
        if (runStateAtLeast(c, targetState) ||
            ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
            break;
    }
}

shutdownNow方法,基本上和shutdown差不多,差别就是多了一个tasks = drainQueue();方法

scss 复制代码
public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(STOP);
        interruptWorkers();
        //清空工作队列并且取出所有的未执行的任务
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

drainQueue方法:

csharp 复制代码
private List<Runnable> drainQueue() {
    //将工作队列赋值给q
    BlockingQueue<Runnable> q = workQueue;
    //初始化一个集合
    ArrayList<Runnable> taskList = new ArrayList<>();
    //将workQueue中的任务 全部放入taskList集合中
    q.drainTo(taskList);
    //某些类型的任务队列poll()和drainTo()方法在移除元素的过程中可能会出现失败的情况
    //比如 DelayQueue 以及其他一些队列
    //在这种情况下,就需要一个一个地转移元素了。
    //所以这里才需要判断q是不是为空 已经移除成功了
    if (!q.isEmpty()) {
        for (Runnable r : q.toArray(new Runnable[0])) {
            if (q.remove(r))
                taskList.add(r);
        }
    }
    return taskList;
}

awaitTermination方法分析

java 复制代码
public boolean awaitTermination(long timeout, TimeUnit unit)
    throws InterruptedException {
    // 转换timeout的单位为纳秒
    long nanos = unit.toNanos(timeout);
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        //如果线程池还没到TERMINATED状态
        while (runStateLessThan(ctl.get(), TERMINATED)) {
            if (nanos <= 0L)
                return false;
            //则等待nanos时间
            nanos = termination.awaitNanos(nanos);
        }
        return true;
    } finally {
        mainLock.unlock();
    }
}

总结

本文主要梳理了线程池ThreadPoolExecutor的核心方法execute()的实现,源码中可以看到,使用到了ReentrantLock,以及AQS的一些同步方法,其实ReentrantLock也是基于AQS的一个实现,后续我会再分析下AQS相关的源码实现。文章中如有错误的地方,还望帮忙指出!感谢各位读者!

相关推荐
llz_1121 小时前
web-第二次课后作业
前端·后端·web
红尘散仙7 小时前
我把终端小说阅读器接上了 AI Agent:TRNovel 现在能用 skill 生成书源了
人工智能·后端·rust
卷毛的技术笔记9 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
会编程的土豆9 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
喵个咪9 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
basketball61610 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
qq_25183645710 小时前
SpringBoot+Vue 共享电池柜管理系统 完整实现 前后端分离项目实战 完整代码
vue.js·spring boot·后端
zhangxingchao10 小时前
AI 大模型核心六:量化、Workflow 与 Agent、多轮 RAG
前端·人工智能·后端
IT_陈寒11 小时前
Vite打包时遇到的坑,原来问题出在这里
前端·人工智能·后端
ayqy贾杰12 小时前
基层管理的三板斧,在AI时代行不通了
前端·后端·团队管理