一文详解ThreadPoolExecutor与线程池原理(上)

一. ThreadPoolExecutor

前文介绍ThreadLocal时提到了许多线程池的场景。在实际项目开发中很少会去手动创建线程,一般的做法都是使用JUC的ThreadPoolExecutor线程池去管理线程。因为创建和销毁线程是十分消耗系统资源和性能的,频繁操作对系统是有损的,使用线程池可以实现统一管理。

  • 降低资源消耗:通过资源复用重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。

这是是一种典型的池化思想。是常见的计算机科学和工程领域的概念,它用于优化资源的使用、提高性能和减少资源浪费。

虽然Executors提供了一些自带的线程池(newCachedThreadPool/newFiexedThreadPool)等,但是在实际项目中一般使用ThreadPoolExecutor根据业务场景设置独立参数。

1. ThreadPoolExecutor参数

探究线程池的参数可以从ThreadPoolExecutor的构造方法上入手,这部分Doug Lea已经为我们写了很详细的注释了

/**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     * @param unit the time unit for the {@code keepAliveTime} argument
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     * @param threadFactory the factory to use when the executor
     *        creates a new thread
     * @param handler the handler to use when execution is blocked
     *        because the thread bounds and queue capacities are reached
     * @throws IllegalArgumentException if one of the following holds:
* {@code corePoolSize < 0}
* {@code keepAliveTime < 0}
* {@code maximumPoolSize <= 0}
* {@code maximumPoolSize < corePoolSize} * @throws NullPointerException if {@code workQueue} * or {@code threadFactory} or {@code handler} is null */ public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }

英文好的直接看注释就可以,英文不好的看我人性化翻译:

corePoolSize :  核心线程数,创建后会保留在线程池中。如果设置了allowCoreThreadTimeOut参数,核心线程同样也会终止。

maximumPoolSize :线程池中的允许创建的最大线程数 。

keepAliveTime :线程闲置存活时长

timeUnit :线程闲置存活时长的时间单位,常用TimeUnit.SECONDS

blockingQueue :任务队列,常用的任务队列包括

ArrayBlockingQueue 一个数组实现的有界阻塞队列,此队列按照 FIFO 的原则对元素进行排序,支持公平访问队列
LinkedBlockingQueue 一个由链表结构组成的可选有界阻塞队列,如果不指定大小,则使用 Integer.MAX_VALUE 作为队列大小,按照 FIFO 的原则对元素进行排序
PriorityBlockingQueue 一个支持优先级排序的无界阻塞队列,默认情况下采用自然顺序排列,也可以指定 Comparator
SynchronousQueue 一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take
DelayQueue 一个支持延时获取元素的无界阻塞队列,创建元素时可以指定多久以后才能从队列中获取当前元素,常用于缓存系统设计与定时任务调度等

threadFactory :线程工厂,用于指定为线程池创建新线程的方式,threadFactory 可以设置线程名称、线程组、优先级等参数。支持自定义线程工厂

RejectedExecutionHandler :拒绝策略,可通过RejectedExecutionHandler接口进行自定义。常见的拒绝策略包括

ThreadPoolExecutor.AbortPolicy 默认策略,当任务队列满时抛出 RejectedExecutionException 异常
ThreadPoolExecutor.DiscardPolicy 丢弃掉不能执行的新任务,不抛任何异常
ThreadPoolExecutor.CallerRunsPolicy 当任务队列满时使用调用者的线程直接执行该任务
ThreadPoolExecutor.DiscardOldestPolicy 当任务队列满时丢弃阻塞队列头部的任务(即最老的任务),然后添加当前任务

2. 线程池是如何执行任务的

首先还是从简单的示例来探究线程池是如何使用的,下面新建了一个线程池。

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                //核心线程数
                2
                //允许的最大线程数
                , 4
                //线程空闲时间
                , 5
                //时间单位-秒
                , TimeUnit.SECONDS
                //指定缓存队列长度为5
                , new LinkedBlockingQueue<>(5)
                //线程工厂
                , new ThreadFactory() {
                      private final AtomicInteger threadNumber = new AtomicInteger(1);
                  @Override
                  public Thread newThread(Runnable r) {
                      Thread thread = new Thread(r);
                      thread.setName("version-demo-pool-" + threadNumber.getAndIncrement());
                      return thread;
                  }
                  }
            //自定义拒绝策略
            , new RejectedExecutionHandler() {
                   @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        //实际项目中是不会抛弃任务的
                        //出现拒绝策略说明线程池的参数已经不满足业务场景了需要做参数修改。
                        //同时对于超出执行能力的任务
                        //可以选择使用其他线程执行或使用消息队列或者存入数据库等待定时任务扫描
                        System.out.println("线程池拒绝策略");
                   }
            });
    executor.execute(()-&gt;{
        System.out.println(Thread.currentThread().getName() + ": 运行线程任务");
    });


---
version-demo-pool-1: 运行线程任务

execute方法是线程池执行任务的入口,对于不了解线程池源码的同学看起来可能有点困难。需要先介绍下execute方法相关的两个重要的概念。

ctl变量

线程池内部使用了一个整型的原子变量ctl来保存线程池的运行状态和线程池内的线程数量。

其中ctl的高3位来表示线程池的状态,ctl的低29位来表示线程池的线程数量。并提供了runStateOf和workerCountOf两个私有方法来获取状态和线程数量

在许多源码中都有会通过一个整型变量的位运算来处理状态的场景比如:

  • ReentrantReadWriteLock是使用一个state变量保存写锁和读锁的获取信息
  • ConcurrentHashMap中使用一个lockState保存三种锁状态
    /**
     * 用来同时记录线程池的运行状态(runState,简称rs)和线程池中线程数量(workerCount,简称wc)。ctl的值使用AtomicInteger原子类包装,能够保证数据是线程安全的。
     * int类型转换为二进制之后的最高三位保存线程池的状态,低29位保存线程数量。刚初始化ctl的时候,rs为RUNNING状态,wc为0
     */
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    /**
     * 线程数量掩码位数,int类型长度-3后的剩余位数,即wc所占位数为29
     */
    private static final int COUNT_BITS = Integer.SIZE - 3;
    /**
     * 为了能正确保存线程数量,线程池的数量线程被限制为29位的最大值,即最大(2^29)-1个,而不是(2^31)-1个
     */
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
//ThreadPoolExecutor中定义了线程池的状态,存储在ctl的高三位中,一共有五种

/**
 * RUNNING状态,11100000000000000000000000000000
 */
private static final int RUNNING    = -1 &lt;&lt; COUNT_BITS;
/**
 * SHUTDOWN状态:00000000000000000000000000000000
 */
private static final int SHUTDOWN   =  0 &lt;&lt; COUNT_BITS;
/**
 * STOP状态:00100000000000000000000000000000
 */
private static final int STOP       =  1 &lt;&lt; COUNT_BITS;
/**
 * TIDYING状态:01000000000000000000000000000000
 */
private static final int TIDYING    =  2 &lt;&lt; COUNT_BITS;
/**
 * TERMINATED状态:01100000000000000000000000000000
 */
private static final int TERMINATED =  3 &lt;&lt; COUNT_BITS;

//通过对ctl的拆解、组合,获取相关的数据

/**
 * 获取ctl的高3位,线程池运行状态
 *
 * @param c 此时的ctl值
 * @return ctl的高3位的int值
 */
private static int runStateOf(int c)     {
    //将c的低29位置为0并返回结果
    return c &amp; ~CAPACITY;
}
/**
 * 获取ctl的低29位,线程数量
 *
 * @param c 此时的ctl值
 * @return ctl的低29位的int值
 */
private static int workerCountOf(int c)  {
    //将c的高3位置为0并返回结果
    return c &amp; CAPACITY;
}

ctl状态对应的线程池生命周期

Worker内部类

Worker类实现了Runnable接口,声明了Thread变量。也就是说线程池中的一个工作线程就是包装后的一个Worker对象。线程池内部使用一个set集合来保存所有已创建的工作线程(也就是Worker的集合)。

   /**
     * 包含全部工作线程的set集合
     * 在线程池统计和销毁是都会用到
     */
    private final HashSet workers = new HashSet();
private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable
{
    /**
     * 此Worker对应的工作线程,由线程工厂创建
     */
    final Thread thread;
    /**
     * 要运行的初始任务,可能为null
     */
    Runnable firstTask;
    /**
     * 用来统计该Worker对应的工作线程完成的任务数
     */
    volatile long completedTasks;

    /**
     * 通过给定的第一个任务和线程工厂创建一个Worker
     *
     * @param firstTask 第一个任务,如果没有就是null
     */
    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        //从线程工厂获取工作线程
        //可以看到这里传递的就是this,这个this代指某个Worker对象本身,因为Worker实现了Runnable,
        //因此实际上返回的thread在start启动之后,会执行对应Worker的run方法
        this.thread = getThreadFactory().newThread(this);
    }

    /**
     * 重写的run方法
     * 实际调用线程池的runWorker方法
     */
    public void run() {
        runWorker(this);
    }
    
    /**忽略其他代码*/

}

看到这里我们对execute方法的探究主要方向是Worker对象是什么情况下创建的、设置的线程池参数之间的关系、缓存队列又是怎么使用的。

execute方法

线程池执行任务有两个方法除了execute方法还有submit方法 。区别在于execute运行的是一个Runnable参数没有返回值。 submit可以运行Callable参数或Runnable参数,有方法返回值

    public void execute(Runnable command) {
        // 空任务抛出异常
        if (command == null) {
            throw new NullPointerException();
        }
        //获取ctl的值,前面提到过。
        //它表示线程状态和线程数量
        int c = ctl.get();
        /**调用workerCountOf计算线程数量,如果小于corePoolSize*/
        if (workerCountOf(c) < corePoolSize) {
            //尝试启动新线程去执行command任务,使用corePoolSize作为线程数量上限
            if (addWorker(command, true)) {
                return;
            }
            //到这里还没有返回
            //表示或者线程池被关闭了,或者线程数量达到了corePoolSize,
            //重新获取ctl的值继续下面判断
            c = ctl.get();
        }
    /*
     * 线程池状态正常情况下向缓存队列添加数据
     * workQueue.offer在有界的缓存队列满是会返回false
     */
    if (isRunning(c) &amp;&amp; workQueue.offer(command)) {
        /** 非主要流程 */
    }
    /*
     * 运行到这里代表核心线程数已满
     * 缓存队列已满
     * 创建新的Worker对象,以最大线程数为数量上线
     */
    else if (!addWorker(command, false)) {
        //最大线程数也满了
        //执行拒绝策略
        reject(command);
    }
}

根据代码流程和注释我们大致捋清下执行流程:

  • 首先线程池接收任务会创建不大于核心线程数的线程
  • 之后会将任务存放在缓存队列中
  • 在缓存队列使用达到上限时会继续创建线程直到等于最大线程数
  • 如果上面三个逻辑都达到使用上限就会触发拒绝策略

除了execute方法ThreadPoolExecutor还提供了submit方法用来获取任务执行后的返回值。

    public  Future submit(Callable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }

3. 线程的创建和销毁

线程池中线程的创建逻辑在addWorker方法,销毁逻辑在runWoker方法。下面对这两个方法对源码进行解析。

addWorker方法

    private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            /**忽略非核心逻辑的代码*/
            /*
             * 开启一个死循环,相当于自旋
             * 判断线程池状态是否正常
             * 因为会有多个任务同时提交的场景,
             * 这部分的作用就是使用CAS方法争抢对addWorker方法的优先使用
             */     
        }
        //到这一步,表示线程池状态和线程数量校验通过,并且已经预先新增了WorkerCount线程数量的值,下面是尝试新增Worker的逻辑
    //workerStarted表示新增的工作线程是否已启动的标志位,初始化为false,表示未启动
    boolean workerStarted = false;
    //workerAdded表示新增的工作线程是否已加入workers集合,初始化为false,表示未加入
    boolean workerAdded = false;
    //w表示需要新增的Worker对象,初始化为nul
    Worker w = null;
    try {
        //新建一个Worker,传入firstTask参数,作为将要执行的第一个任务
        //在构造器中还会通过线程工厂初始化一个新线程
        w = new Worker(firstTask);
        //获取w内部的新线程t
        final Thread t = w.thread;
        //如果t不为null
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                //获取此时的线程池运行状态值rs
                int rs = runStateOf(ctl.get());
                /*
                 * 如果rs小于SHUTDOWN,即属于RUNNING状态
                 * 或者 rs属于SHUTDOWN状态,并且firstTask为null(不是新增任务的请求)
                 *
                 * 满足这两个条件中的一个,才可以真正的开启线程
                 */
                if (rs &lt; SHUTDOWN ||
                    (rs == SHUTDOWN &amp;&amp; firstTask == null)) {
                    /*
                     * 继续校验线程t是否是活动状态,因为如果线程已经处于活动状态,表示已经执行了start()方法,即已经开始执行了run方法
                     * 那么这个线程就不能再执行新任务,不符合要求,由调用方直接抛出IllegalThreadStateException异常
                     * 这里也要求线程工厂返回的线程,仅仅是一个Thread对象即可,不能够start启动而帮倒忙!
                     */
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    //新建的Worker加入workers的set集合
                    workers.add(w);
                    //获取workers的数量s,就表示目前工作线程数量
                    int s = workers.size();
                    //如果s大于largestPoolSize,即大于历史最大线程数量
                    if (s &gt; largestPoolSize)
                        //那么largestPoolSize更新为s
                        largestPoolSize = s;
                    //workerAdded置为true,表示新增的工作线程已加入workers集合
                    workerAdded = true;
                }
            } finally {
                //解锁
                mainLock.unlock();
            }
            //判断是否新增成功
            if (workerAdded) {
                //工作线程就绪后面在线程执行时会调用worker重写的run方法
                t.start();
                //workerStarted置为true,表示新增的工作线程已启动
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            //添加失败走addWorkerFailed对此前可能做出的改变进行“回滚”
            addWorkerFailed(w);
    }
    return workerStarted;
}

虽然这部分代码很长但是耐心看下来逻辑并不复杂。首先就是校验线程池状态,然后通过线程数的CAS方法来争抢使用权。之后的逻辑就是创建Worker对象,成功条件下调用线程的start方法让线程就绪并且处理其他相关数据状态后返回 。重点在于Worker就绪后会调用重写的run方法来执行任务逻辑

runWorker方法

    final void runWorker(Worker w) {
        //获取当前工作线程wt
        Thread wt = Thread.currentThread();
        //获取第一个要执行的task记录下来
        Runnable task = w.firstTask;
        //w.firstTask置空,释放引用
        w.firstTask = null;
        w.unlock(); // allow interrupts
        //线程执行过程中是否发生异常的标志位,初始化为true
        boolean completedAbruptly = true;
        try {
            /*
             * 在这个循环下满足true的条件才有去执行任务
             * 第一个判断task != null 指的是新建worker的场景会一个初始任务
             * 第二个判断(task = getTask()) != null 指的是去缓存队列里面拿任务
             * 都不满足跳出循环,也就是说一直能拿到任务这个while循环是一直存在的
             */
            while (task != null || (task = getTask()) != null) {
                w.lock();
                if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) {
                    wt.interrupt();
                }
                try {
                    //任务开始执行的前置方法,默认空实现,自定义的子类可以实现自己的逻辑
                    beforeExecute(wt, task);
                    //thrown记录任务执行过程中抛出的异常
                    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;
                    //此Worker记录的完成任务数量自增1
                    w.completedTasks++;
                    //解锁,将state置为0
                    w.unlock();
                }
            }
            //到这一步,表示task为null,并且通过getTask中任务队列获取的任务也为null,即没有任务可执行了
            //这是正常行为,completedAbruptly置为false
            completedAbruptly = false;
        } finally {
            //无论上面有没有抛出异常,都会执行finally语句块。可能是执行过程中抛出了异常,或者getTask返回null
            //getTask返回null,则可能是各种各样的情况,比如超时、比如线程池被关闭等
            //调用processWorkerExit方法,传递Worker对象以及是否发生异常的标志位
            //processWorkerExit方法会将该Worker移除workers集合,并且根据completedAbruptly决定是否新建Worker添加到workers集合
            processWorkerExit(w, completedAbruptly);
        }
    }

这段代码总体逻辑就是在while循环中获取task然后执行,如果跳出了while循环或者是出现了异常或者是没有可执行的任务了。对于while的两个条件

第一个判断task != null 指的是新建worker的场景会一个初始任务
第二个判断(task = getTask()) != null 指的是去缓存队列里面拿任务
都不满足跳出循环。
也就是说一直能拿到任务这个while循环是一直存在的(线程复用)

下面的getTask的源码,通过allowCoreThreadTimeOut参数和当前线程数是否大于核心线程数的两个条件判断是使用缓存队列的take方法(如果队列为空会一直等待直到队列中有任务添加)还是poll方法(超过等待时间获取不到数据会返回null)获取任务。

    private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out
        for (;;) {    
            /*
             * 省略非核心代码 
             */
            //获取线程数量wc
            int wc = workerCountOf(c);
        /*
         * timed表示对工作线程是否应用超时等待的标志
         * 如果allowCoreThreadTimeOut为true,表示所有线程都应用超时等待
         * 或者 wc大于核心线程数量,那么可以对超过corePoolSize的线程应用超时等待
         * 以上情况满足一种,timed即为true,否则为false
         */
        boolean timed = allowCoreThreadTimeOut || wc &gt; corePoolSize;

        /*
         * 省略非核心代码 
         */
        
        try {
            /*
             * 判断timed是否为true
             * 如果为true,那么需要对线程应用超时等待,调用workQueue的超时poll方法,
             * 在超时时间范围内等待获取并移除任务(队头),如果超时时间没有获取道任务,那么返回null
             * 如果为false,那么不需要对线程应用超时等待,调用workQueue的take方法,
             * 获取并移除任务(队头),没有获取到任务将会一直等待
             *
             * 返回值使用r变量接收
             */
            Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

至此对于线程池我们对它的线程创建和任务执行的整体逻辑都已清楚。果然看了源码就没有秘密

4. 示例

    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                //核心线程数
                2
                //允许的最大线程数
                , 4
                //线程空间时间
                , 60
                //时间单位-秒
                , TimeUnit.SECONDS
                //指定缓存队列长度为5
                , new LinkedBlockingQueue<>(5)
                //线程工厂
                , new ThreadFactory() {
                      private final AtomicInteger threadNumber = new AtomicInteger(1);
                  @Override
                  public Thread newThread(Runnable r) {
                      Thread thread = new Thread(r);
                      thread.setName("version-demo-pool-" + threadNumber.getAndIncrement());
                      return thread;
                  }
                  }
            //自定义拒绝策略
            , new RejectedExecutionHandler() {
                   @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        //实际项目中是不会抛弃任务的
                        //出现拒绝策略说明线程池的参数已经不满足业务场景了需要做参数修改。
                        //同时对于超出执行的能力的任务
                        //可以选择使用其他线程执行或使用消息队列或者存入数据库等待定时任务扫描
                        System.out.println("线程池拒绝策略");
                   }
            });

    for(int i = 0 ;i &lt; 12 ; i++){
        executor.execute(()-&gt;{
            System.out.println(String.format("%s 运行线程任务,当前线程数 %s,当队列长度%s",Thread.currentThread().getName(),executor.getPoolSize(),executor.getQueue().size()));
            try {
                //休眠模拟任务占用
                Thread.sleep(5 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        try {
            //休眠模拟任务占用
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

二. ScheduledThreadPoolExecutor

在JUC的包中除了ThreadPoolExecutor还提供了ScheduledThreadPoolExecutor用来执行延迟任务或定期任务的线程池。ScheduledThreadPoolExecutor继承自ThreadPoolExecutor对于线程池的核心逻辑没有太大变动。

public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService {
//ScheduledThreadPoolExecutor的构造方法直接使用了父类的方法
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory,
                                   RejectedExecutionHandler handler) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue(), threadFactory, handler);
}

}

ScheduledThreadPoolExecutor的构造方法没什么特殊之处直接调用了父类的构造方法。但是对之前讲的线程池核心参数ScheduledThreadPoolExecutor有不同的设置。最大线程数设置了Integer的最大值,我们都知道线程数是不能无限使用的,受制于机器的配置线程数是有限的。所以可以猜测最大线程数这个参数对于ScheduledThreadPoolExecutor是没有使用逻辑的

另外ScheduledThreadPoolExecutor使用了一个特殊的缓存队列DelayedWorkQueue,可以确定ScheduledThreadPoolExecutor实现延迟任务和定时任务的核心就在于此。

1. DelayedWorkQueue

对于DelayedWorkQueue可以先看下作者的注释

/ * A DelayedWorkQueue is based on a heap-based data structure * like those in DelayQueue and PriorityQueue, except that * every ScheduledFutureTask also records its index into the * heap array * /*

DelayedWorkQueue基于堆的数据结构就像DelayQueue和PriorityQueue中的那些,除了每个ScheduledFutureTask还将其索引记录到堆数组。

DelayedWorkQueue是堆的数据结构也称之为优先队列,在这里它是一个小顶堆。可以理解为每次新增数据都会把最小(执行时间最近的任务)的数据排在队头,取数据时只获取队头的任务来执行。对于堆的数据结构可以通过经典排序算法堆排序来了解

        //DelayedWorkQueue对于延迟任务的初始定义
        private static final int INITIAL_CAPACITY = 16;
        private RunnableScheduledFuture[] queue =
            new RunnableScheduledFuture[INITIAL_CAPACITY];

另外还需要说明一下DelayedWorkQueue中的存储的结构是RunnableScheduledFuture对象。我们在向缓存队列提交任务时通常是个Runnable类型。基于DelayedWorkQueue的特殊性,它不光需要存储任务Runnable还需要记录任务延迟时间,是否需要重复执行等参数。所以需要RunnableScheduledFuture这么一个对象间接继承Runnable类型,除了存储任务还额外存储延迟时间、是否重复执行和返回值类型的信息。

    /**
     * ScheduledThreadPoolExecutor内部的专用延迟任务
     */   
     private class ScheduledFutureTask
            extends FutureTask implements RunnableScheduledFuture {       
    /**
     * 创建一个在指定纳秒时间之后执行的一次性任务,具有指定返回结果
     *
     * @param ns     任务延迟执行时间点纳秒
     * @param r      任务
     * @param result 指定返回结果
     */
    ScheduledFutureTask(Runnable r, V result, long ns) {
        //调用父类的构造器
        super(r, result);
        this.time = ns;
        this.period = 0;
        this.sequenceNumber = sequencer.getAndIncrement();
    }

}

DelayedWorkQueue在ScheduledFutureTask入队时的比较逻辑,可以看出是按照执行时间的先后来排序的,如果执行时间一致则按照任务序号sequencer来判断。

       public int compareTo(Delayed other) {
            if (other == this) // compare zero if same object
                return 0;
            if (other instanceof ScheduledFutureTask) {
                ScheduledFutureTask x = (ScheduledFutureTask)other;
                long diff = time - x.time;
                if (diff < 0)
                    return -1;
                else if (diff > 0)
                    return 1;
                else if (sequenceNumber < x.sequenceNumber)
                    return -1;
                else
                    return 1;
            }
            long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
            return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
        }

2. 任务执行

对任务执行的探究还是从最简单的execute方法开始,ScheduledThreadPoolExecutor重写了execute方法和submit方法的逻辑,都会调用到schedule方法。

    public void execute(Runnable command) {
        schedule(command, 0, NANOSECONDS);
    }
public <t> Future<t> submit(Runnable task, T result) {
    return schedule(Executors.callable(task, result), 0, NANOSECONDS);
}

schedule方法没有太多逻辑,调用了decorateTask只是返回了一个新建ScheduledFutureTask类。前面提到了DelayedWorkQueue中的存储的是RunnableScheduledFuture对象。主要逻辑在于delayedExecute这个方法中

    public ScheduledFuture schedule(Runnable command,
                                       long delay,
                                       TimeUnit unit) {
        if (command == null || unit == null)
            throw new NullPointerException();
        //调用decorateTask
        //没有特殊逻辑只是返回了返回了封装的ScheduledFutureTask
        RunnableScheduledFuture t = decorateTask(command,
            new ScheduledFutureTask(command, null,
                                          triggerTime(delay, unit)));
        //主要执行逻辑
        delayedExecute(t);
        return t;
    }

delayedExecute

下面这部分代码和ThreadPoolExecute的最大区别在于,ScheduledThreadPoolExecutor不是先创建核心线程数来执行任务,而是接收到任务就放入队列中。让队列按照执行时间的先后排好序再通过线程执行。

    private void delayedExecute(RunnableScheduledFuture task) {
        /*如果线程池已关闭(非RUNNING状态),直接执行拒绝策略*/
        if (isShutdown())
            reject(task);
        else {
            /*
             * 首先将task任务通过DelayedWorkQueue的add方法加入到阻塞队列,该方法就是通过新元素构建小顶堆的逻辑
             * 每次增加元素后都会重新排列,将最小数据(时间最近的任务)排到队头
             */
            super.getQueue().add(task);
            /*
             * 非主要逻辑
             */
            if (isShutdown() &&
                !canRunInCurrentRunState(task.isPeriodic()) &&
                remove(task))
                task.cancel(false);
            else
                /*
                 * 任务可执行,创建线程
                 */
                ensurePrestart();
        }
    }

下面这部分的代码看到了我们熟悉的ctl变量和addWoker方法,这块的逻辑就是当前线程数小于核心线程数或者当前线程数为0时需要创建Worker对象来执行任务。有个特殊之处addWorker方法传入的任务是空,那么就代表着在runWorker获取任务时只能从通过getTask方法来获取任务(可以回忆上面runWoker方法while里面的两个判断)。也就是说任务的来源只来自于DelayedWorkQueue的take方法/poll方法。

    void ensurePrestart() {
        int wc = workerCountOf(ctl.get());
        if (wc < corePoolSize)
            addWorker(null, true);
        else if (wc == 0)
            addWorker(null, false);
    }

稍加思索大致可以捋清延迟执行的实现思路了。在执行任务时只是向优先队列里面添加数据,任务的执行完全看什么时候能从优先队列里面获取队头的任务(由任务的延迟时间决定)。拿到任务后优先队列再每项前移。

        //DelayedWorkQueue的poll方法源码
        public RunnableScheduledFuture poll() {
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                RunnableScheduledFuture first = queue[0];
                if (first == null || first.getDelay(NANOSECONDS) > 0)
                    return null;
                else
                    return finishPoll(first);
            } finally {
                lock.unlock();
            }
        }

上面的逻辑只是说清楚了ScheduledThreadPoolExecutor是怎么延迟执行任务的。但是对于怎么定期执行任务的还是没有说清楚。这部分的逻辑在ScheduledFutureTask中,它间接继承了Runnable也就是会重写run方法来实现特殊逻辑。

        public void run() {
            //periodic表示当前任务是否是周期性任务
            //就是看period是否等于0
            boolean periodic = isPeriodic();
            if (!canRunInCurrentRunState(periodic))
                cancel(false);
            /*
             * 否则,表示可以执行。继续判断是否是一次性任务
             * 如果是一次性任务,那么调用ScheduledFutureTask的父类FutureTask的run方法执行任务
             * 而在FutureTask的run方法中,有会调用c.call()方法,这里面才是我们自己写的任务逻辑
             */
            else if (!periodic)
                ScheduledFutureTask.super.run();
                /*
                 * 否则,表示周期性任务。
                 * 那么调用父类FutureTask的runAndReset方法执行任务并且执行完毕之后重置任务
                 */
            else if (ScheduledFutureTask.super.runAndReset()) {
                //执行并重置成功之后,设置任务下一次要执行的时间点
                setNextRunTime();
                //下一次待执行的任务放置到DelayedWorkQueue中,这个outerTask默认指向该任务自己
                reExecutePeriodic(outerTask);
            }
        }
    }

3. 周期任务

在实际开发中对于的使用很少会调用execute和submit方式,更多的使用scheduleWithFixedDelay和scheduleAtFixedRate两个方法来执行周期任务。两个方法都可以定期执行任务,区别在于:

scheduleAtFixedRate:是每次任务执行时间开始就计算时间间隔

scheduleWithFixedDelay:是每次任务执行结束才开始计算时间间隔

scheduleAtFixedRate和 scheduleWithFixedDelay的源码实现和schedule没有太大的逻辑差异。这部分的源码就不做深入分析了,我们从使用角度直观的看下二者的区别。

        ScheduledThreadPoolExecutor threadPool = new ScheduledThreadPoolExecutor(2);
        //第一个参数表示执行任务
        //第二个参数表示延迟执行的时间,0表示立即执行
        //第三个参数表示周期间隔时间2秒
        //第四个参数时间单位
        threadPool.scheduleAtFixedRate( ()->{
            System.out.println(System.currentTimeMillis() + " scheduleAtFixedRate周期任务");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },0,2, TimeUnit.SECONDS);

       ScheduledThreadPoolExecutor threadPool = new ScheduledThreadPoolExecutor(2);
        //第一个参数表示执行任务
        //第二个参数表示延迟执行的时间,0表示立即执行
        //第三个参数表示周期间隔时间2秒
        //第四个参数时间单位
        threadPool.scheduleWithFixedDelay( ()->{
            System.out.println(System.currentTimeMillis() + " scheduleWithFixedDelay周期任务");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },0,2, TimeUnit.SECONDS);

三. 总结

看到这里想必大家对线程池的使用和原理都有了很深的理解。文章的部分源码对新手可能不太友好,但是耐心看完是一定会有帮助的。另外由于篇幅有限其中还涉及到某些细节没有展开来讲,后面可以单独针对某一细节完成细致的源分析文章。对于线程池下半部分的规划是介绍ForkJoinPool和如何更好的设置线程池参数。如果大家有其他对于线程池感兴趣的内容欢迎留言。


这是一个从 https://juejin.cn/post/7368836713965977639 下的原始话题分离的讨论话题