扒一扒 ScheduledThreadPoolExecutor

目录

前言 

如何使用 ScheduledThreadPoolExecutor?

ScheduledExecutorService

ScheduledThreadPoolExecutor

构造函数

DelayedWorkQueue

        DelayedWorkQueue 如何组织数据?

        DelayedWorkQueue 如何操作数据?

ScheduledFutureTask

ScheduledThreadPoolExecutor 到底是如何实现周期性执行任务的?


前言 

        因为 ScheduledThreadPoolExecutor 是 ThreadPoolExecutor 的子类,它只是对 ThreadPoolExecutor 进行一些功能的扩充,所以好多核心原理都是在 ThreadPoolExecutor 实现的。因为之前都已经聊过了,所以本篇文章不会再重复。所以建议先把前面的文章看完理解之后再来看本篇文章。关于 Java 线程与线程池的那些事https://blog.csdn.net/paralysed/article/details/122765416?spm=1001.2014.3001.5502

如何使用 ScheduledThreadPoolExecutor?

public class ScheduledExecutorServiceTest {
    public static void main(String[] args) throws Exception {
        // 实例化线程池
        ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
            new BasicThreadFactory.Builder().namingPattern("cison-%d").daemon(false).build());

        // 设置模拟任务需要执行 2s
        MyTask myTask = new MyTask(2000);

        System.out.println("起始时间: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));

        // 延时 1 秒后,按 5 秒的周期执行任务
        executorService.scheduleAtFixedRate(myTask, 1000, 5000, TimeUnit.MILLISECONDS);
        //executorService.scheduleWithFixedDelay(myTask, 1000, 5000, TimeUnit.MILLISECONDS);
    }

    private static class MyTask implements Runnable {

        private final int workTime;
        private final SimpleDateFormat dateFormat;

        public MyTask(int workTime) {
            this.workTime = workTime;
            dateFormat = new SimpleDateFormat("HH:mm:ss");
        }

        @Override
        public void run() {
            System.out.println("任务开始,当前时间:" + dateFormat.format(new Date()));

            try {
                System.out.println("任务执行中...");
                Thread.sleep(workTime);
            } catch (InterruptedException ex) {
                ex.printStackTrace(System.err);
            }

            System.out.println("任务结束,当前时间:" + dateFormat.format(new Date()));
            System.out.println();
        }
    }
}

//打印结果
起始时间: 15:17:26
任务开始,当前时间:15:17:27
任务执行中...
任务结束,当前时间:15:17:29

任务开始,当前时间:15:17:32
任务执行中...
任务结束,当前时间:15:17:34

        可以看出来,使用 ScheduledThreadPoolExecutor 来提交需要定时执行的任务是非常简单的,上面的任务会在程序任务提交后 1s 开始执行,此后每隔 5s 就执行一次。即两次任务开始的是时间间隔为 5s。

        有人可能就发现了,这里直接无视了任务的执行时间,也就是说无论这个线程提交的任务执行多久,都不影响我 5s 执行一次。但是有的时候我们可能需要这样一个场景,就是当前任务执行结束后等待 5s 再执行下一次。这个时候需要将上面的提交语句替换为代码中注视掉的一部分,即使用 scheduleWithFixedDelay 的方式进行提交。

ScheduledExecutorService

        Executor 是线程池相关最顶层接口,现在开发规范所有的线程池都要继承 Executor, 但是它只定义了一个 execute 方法,完全不够用呀~所以随着线程池技术的发展,有些业务场景需要线程池周期性的执行提交的任务,这个时候就提出了 ScheduledExecutorService 接口,我们知道接口一般都是定义规范,所以我们把它理解为周期性线程池的祖先吧,后面任何一种有关周期性的线程池都会继承它,所以出于尊敬,我们看看它到底定义了什么规范。

public interface ScheduledExecutorService extends ExecutorService {

    // 这个方法定义了可以使用该方法提交任务,但是只会执行一次,且在提交任务之后 delay 执行
    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay, TimeUnit unit);

    // 这个方法跟上面那个基本一样,唯一不同的是它会有返回值,将任务执行的结果返回
    public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                           long delay, TimeUnit unit);

    // 这个方法定义了任务在提交后延时 initialDelay(ms) 时间执行,然后每两次任务开始之间的间隔为 initialDelay,单位是 unit
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);

    // 这个方法定义了任务在提交后延时 initialDelay(ms) 时间执行,然后每次任务结束之后休息 initialDelay,单位是 unit,然后再执行下一次任务
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);

}

        可以看到,该接口定义了4个规范,其中前两个都是一次性的任务比较简单,本篇文章就不聊了,后面 2 个方法我们在开头已经用代码验证过,相比大家都已经知道它们俩是怎么回事,以及二者有什么区别。

ScheduledThreadPoolExecutor

        有了接口就必须要有实现,否则干定义一堆规范并没有什么卵用。ScheduledThreadPoolExecutor 就是 ScheduledExecutorService 接口的一个实现。我们现在也不看它具体怎么实现的,可是理论上就只需要重写上面 4 个方法就好了,可是我们闭着眼睛也能想出来(闭着眼睛当然能想~),上面这四个方法跟线程没有毛的关系,更不用说线程池的机制来维护一堆线程了。那么我们要怎么实现 ScheduledExecutorService 才能让它变成一个正常的线程池呢?

        看了前面 ThreadPoolExecutor 文章的应该可以想到,ThreadPoolExecutor 即然它已经维护了一套完整的线程池机制,那么我们直接复用它的机制不就好了么。ThreadPoolExecutor 怎么维护线程我就怎么维护线程,它怎么执行任务我就怎么执行任务。所以 ScheduledThreadPoolExecutor 还需要继承 ThreadPoolExecutor,以此来复用那一套完整的维护线程池的机制。然后结合 ScheduledExecutorService 规范再稍微改一改,不就成了一个可以周期性执行任务的线程池了么。            从上面的代码样例我们应该可以看出来,实现周期性执行任务的线程池主要有两行代码,一是实例化线程池,二是提交任务,至于定义任务的逻辑不是本文的重点,所以我们就着重分析下这两步到底干了啥吧

ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
            new BasicThreadFactory.Builder().namingPattern("cison-%d").daemon(false).build());

executorService.scheduleAtFixedRate(myTask, 1000, 5000, TimeUnit.MILLISECONDS);

构造函数

//这只是本例用到的一个构造函数,还有其它的构造函数,不过原理一样,所以本文就只分析这个了
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    //调用父类的构造方法
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue(), threadFactory);
}

// 这里调用 ThreadPoolExecutor 的构造方法,之前已经讲过了,这里就不再重复了。
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}

        这里看好像并没有干什么,我们之前讲 ThreadPoolExecutor 举例的时候也是用了这个构造方法实例化了一个线程池,那么这个通过父类调用好像跟之前那个是一样的呀,它怎么就可以周期执行任务了呢?

        这里的秘诀在于第五个参数,ScheduledThreadPoolExecutor 传递的是 DelayedWorkQueue。那么这个队列是什么呢?

DelayedWorkQueue

        不看不知道,一看吓一跳呀,它是 ScheduledThreadPoolExecutor 的一个内部类,这个类的内容占用了 ScheduledThreadPoolExecutor 超过一半的代码,看来它是很重要的一部分,这里我分为两部分讲这部分队列内容,第一部分是 DelayedWorkQueue 如何组织数据,第二部分是 DelayedWorkQueue 如何操作数据。

        DelayedWorkQueue 如何组织数据?

static class DelayedWorkQueue extends AbstractQueue<Runnable>
        implements BlockingQueue<Runnable> {

        private static final int INITIAL_CAPACITY = 16;
        private RunnableScheduledFuture<?>[] queue =
            new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
        private final ReentrantLock lock = new ReentrantLock();
        private int size = 0;
}

        从上面的信息可以看出来 DelayedWorkQueue 内部维护了一个数组 queue, 他的初始大小为 16,但是难道 DelayedWorkQueue 真的只是一个简单的数组吗?我们知道一般数组在进行删除操作的时候是很费劲的,首先要把删除的节点移除掉,然后再把后面的所有元素都往前移动。此外 DelayedWorkQueue 还是一个优先队列,队头元素永远是按照我们定义的排序规则中优先级最高的节点。数组进行排序最快的时间复杂度也是 O(nlog(n)), 对于队列这种常常进行新增,删除和查询操作的数据结构来说,显然使用普通数组的方式来维护数据是不合适的。

        所以虽然这里维护了一个数组,但是并不是简单的像我们平时使用数组笨笨的维护和操作数据了。而是使用数组来模拟堆的实现。如果大家对堆这个数据结构不了解,可以先去补一下堆相关的知识,可以理解为它是模拟逻辑的二叉树结构,比如下标 0 是 1,2 的逻辑父节点,且 0 的优先级比 1 和 2 都要高,同理 1 的优先级比 3 和 4 的优先级都要高。但是需要注意的是,兄弟节点比如 1 和 2 之间的优先级是不确定的。否则它就变成二叉排序树了。也就是说数据确实是存在数组里的,但是数据存取的方式是按照堆的逻辑进行的,物理上它是个数组,逻辑上是个堆。

        堆又分小顶堆和大顶堆,小顶堆就是根节点元素值最小。但是为什么我们上面一直强调的是优先级呢?因为无论是大顶堆还是小顶堆,那都是要服务于我们的业务的,不是仅仅为了大而大,为了小而小。如果我觉得元素值小的具有较高的优先级,那么我就用小顶堆;如果我的业务需要让元素值大的拥有较高的优先级,那么我就用大顶堆。所以可以看出来无论是大顶堆还是小顶堆,对于我们的业务来说,从上到下优先级都是递减的。

        只考虑优先级也可以方便代码的编写,我们知道队列中存放的是一个个对象,对象都是可以实现 Comparable 接口并重写 compare 方法来自定义排序规则的,那么我们是不是可以把定义优先级的逻辑从队列中抽离出来呢?答案是可以的,队列的堆在维护数据的时候确定把元素值小的排在前面,元素值大的排在后面;不用再进行复杂的逻辑判断,所以也简化了代码的开发。那么元素值小还是大由谁定义呢?就是由对象的 compare 方法,所以说堆的排序实现是确定的,而优先级交由用户自己定义,这样可以说既简单又有更大的扩展性。这里可以说一下,DelayedWorkQueue 使用的是小顶堆的实现方式,即 DelayedWorkQueue 就认为元素值越小优先级就越高,那么我们自定义 compare 方法要适配这个原则,比如我举个例子

public class People implements Comparable{
    private Integer age;
    
    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    // 如果想要年龄小的拥有较大的优先级,需要按照如下方式重写 compareTo 方法
    @Override
    public int compareTo(Object o) {
        People other = (People) o;
        return age.compareTo(other.getAge());
    }

    // 如果想要年龄大的拥有较高的优先级,需要按照如下方式重写 compareTo 方法
    /*@Override
    public int compareTo(Object o) {
        People other = (People) o;
        return other.getAge().compareTo(age);
    }*/
}

        因为 DelayedWorkQueue 面向我们用户层它是个队列,但是内部却是用数组来存储数据的,更关键的是它是使用堆的逻辑来组织数据的,所以下面我在不同的场景可能会用不同的描述,比如在使用层面的时候,我说队列那就是在说 DelayedWorkQueue,在说数据存储的时候那着眼点肯定是数组,在聊数据的读取原理的时候,那肯定就是逻辑层面的堆~

        DelayedWorkQueue 如何操作数据?

// 入队操作
public boolean offer(Runnable x) {
    if (x == null)
        throw new NullPointerException();
    // 因为数组元素是 RunnableScheduledFuture 类型的,所以需要将传入的 Runnable 对象转为 RunnableScheduledFuture 类型
    RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
    // 执行入队之前需要进行加锁操作,同一时刻只能由一个线程能够进行入队操作
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        int i = size;
        // 如果数组空间不够了,需要进行扩容
        if (i >= queue.length)
            grow();
        size = i + 1;
        if (i == 0) {
            //如果是第一次入队操作,因为不涉及排序,所以直接放到数组中下标为 0 的位置即可
            queue[0] = e;
            // 设置节点 e 的 index 为 0
            setIndex(e, 0);
        } else {
            // 当堆中已经有了元素之后,因为要维护顺序,所以需要调用 siftUp 方法执行入队操作
            siftUp(i, e);
        }
        if (queue[0] == e) {
            //这里暂时先不管,后面再讲
            leader = null;
            available.signal();
        }
    } finally {
        // 执行完成之后解锁
        lock.unlock();
    }
    return true;
}

/**
 * 每次往堆中增加一个节点的时候,需要调用该方法把节点放到正确的位置
 * 所谓正确的位置,就是要满足堆的性质,父节点优先级要比孩子节点优先级高
 * 这里两个参数 k 和 key 代表准备把 key 节点放在 k 的位置,但是如果 k 的父节点优先级比 key 还低,就把 k 对应的节点拖下来,把 key 移上去。然后依次类推,直到找到父节点优先级比 key 高或者遍历到根节点才停止
 * 从上面的过程可以看出,每次入队操作伴随着堆中多个节点的移动,而不是简单把节点放到数组最后一个位置
 */
private void siftUp(int k, RunnableScheduledFuture<?> key) {
    while (k > 0) {
        // 找到父节点下标,比如下标 2 的父节点是 0,1 的父节点也是 0,所以找父节点的方式就是 (k - 1) >>> 1
        int parent = (k - 1) >>> 1;
        // 获取到节点元素
        RunnableScheduledFuture<?> e = queue[parent];
        // 前面我们分析 DelayedWorkQueue 采用小顶堆来实现,就是在这里体现的,如果 key 节点元素值比较大,说明优先级较小,那么就不能再把父节点拖下来了,跳出循环把 key 放到 k 下标就好。
        if (key.compareTo(e) >= 0)
            break;
        // 如果父节点元素值比 key 的元素值要大,说明其优先级较小。此时将父节点 e 往下挪一挪,放到 k 的位置,这里可能会有疑问,比如 k 还有一个左边的兄弟 L,把父节点拿了下来不是父节点排在 L 后面了么,这样不对呀
        // 这就是涉及到堆的机制了,堆并不是二叉排序树,堆只能保证父节点排在子节点前面,但是兄弟节点是不管的,所以根节点一定是按照排序规则的最优先的节点,每次取出堆顶元素之后,都会重新选举新的根节点
        // 所以堆的插入,查找和删除操作对应的时间复杂度都是 O(logn),因为队列往往都伴随着频繁的插入操作,所以使用堆来维护是很合适的
        queue[k] = e;
        // 修改父节点的 index 为 k
        setIndex(e, k);
        //此时将 k 设置为 parent 的意思是指打算把 key 这个节点放入 parent 这个下标处,然后再次循环看看这个位置是不是最终的理想地
        k = parent;
    }
    // 跳出循环说明找到比 key 优先级更高的父节点所以执行 break 跳出循环;或者当前堆中的元素优先级都比 key 的低,那么最终 k == 0, 不满足 while 的控制条件而跳出循环,那么根节点0就是它的理想归宿
    queue[k] = key;
    // 设置任务 key 的 index
    setIndex(key, k);
}

// 最后看一下 setIndex 方法,该方法没什么处理逻辑,就是给 ScheduledFutureTask 类型的对象的 heapIndex 赋值,它就代表在数组中的下标
private void setIndex(RunnableScheduledFuture<?> f, int idx) {
    if (f instanceof ScheduledFutureTask)
        ((ScheduledFutureTask)f).heapIndex = idx;
}

        上面的几个函数是入队操作涉及到的动作,相信看了这几个函数就知道 DelayedWorkQueue 怎么利用数组来模拟实现一个堆了。我们梳理一下上面的逻辑,当有新元素要入队的时候,首先我们会尝试把该元素放入数组中最后一个下标的位置,但是放入之前需要先进行下判断,如果父节点的优先级比当前节点优先级要低,那么就把父节点拖下来放到最后一个下标的位置,然后尝试把新元素放到父节点下标的位置,但是这个时候还是要经过相同的判断逻辑,即跟新的父节点进行优先级比较,如果新的父节点还是没有新元素优先级高,那么新的父节点也要下来,新的继续往上爬,一直爬到根节点或者遇到有的父节点比当前优先级更高为止。

        那么我们此时可以猜测一下,如果是出队操作该怎么处理呢?首先出队肯定是出的数组的第一个元素,那么此时根节点就空白了,需要找新的元素节点顶上来,那么找谁顶替呢?此时我们还需要注意的一点是因为队列中少了一个元素,那么数组中最后一个元素的位置其实就不需要了,比如之前有 6 个元素,现在只有 5 个了,那么我们只需要数组的前五个位置就可以保存所有的数据了。

        我们可以采用跟入队相反的操作来进行处理,我们把数组中最后一个元素拿出来,我们把它成为尾节点,因为目前根节点位置是空的,所以我们先尝试把尾节点放入根节点的位置,但是放入之前需要判断一下它的两个孩子节点是否有更高的优先级,如果孩子的优先级更高,那么就把孩子拉上来,当前元素往下走去准备放孩子的位置那里,这里有两个需要注意的问题,第一就是如果两个孩子的优先级都比当前元素优先级高怎么办呢?那就是把两者优先级相对较高的提上来,因为堆本来也就只保证父节点比孩子节点优先级高,但是兄弟节点之间无所谓的。第二个需要注意的问题就是当把孩子提上来之后,孩子节点的位置现在空了,但是还是不能直接把尾节点放到空出来的位置上(否则可能会破坏堆的性质,即父节点一定比孩子节点优先级高),而是继续判断新位置的孩子节点是不是可以继续往上拖,如此反复,最终把优先级高的节点都提了上来,尾节点最终也可以找到最终的归宿。下面我们就来看一下代码吧

// 出队方法
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();
    }
}

private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) {
    //因为队列少了一个元素,所以设置 size - 1 代表当前队列中元素个数少了一个,此外因为队列前面空了一个位置,队列中之前放到最后一个位置的元素应该要移到前面去了
    int s = --size;
    RunnableScheduledFuture<?> x = queue[s];
    queue[s] = null;

    if (s != 0)
        // 把之前放到队列最后面的元素重新入到队列中合适的位置
        siftDown(0, x);

    // 队头元素被弹出,设置该元素的 index 为 -1
    setIndex(f, -1);
    // 返回队头元素
    return f;
}


/**
 * 该方法的思路跟 siftUp 刚好相反,思路就是我先尝试把 key 节点放入堆顶,但是如果它的孩子有比 key 靠前的,那么就把靠前的孩子节点拖上来,然后再尝试把 key 节点放到孩子节点的位置上,如果孩子也不能放就一直往下移动
 */
private void siftDown(int k, RunnableScheduledFuture<?> key) {
    int half = size >>> 1;
    while (k < half) {
        // 确定第左孩子节点的下标
        int child = (k << 1) + 1;
        // 拿到左孩子节点的元素
        RunnableScheduledFuture<?> c = queue[child];
        // 确定右孩子的下标
        int right = child + 1;
        // 取左孩子和右孩子中较小的那一个
        if (right < size && c.compareTo(queue[right]) > 0)
            c = queue[child = right];
        // 如果 key 比孩子节点还要靠前,那么说明 key 就是可以放到当前 k 的下标处的
        if (key.compareTo(c) <= 0)
            break;
        //说明孩子节点更靠前,把孩子节点拉上来
        queue[k] = c;
        // 修改孩子节点的 index
        setIndex(c, k);
        // 这句的意思是尝试把 key 节点放到 child 位置,然后再次循环判断能不能放到 child 的位置上
        k = child;
    }
    //最终找到了合适的位置
    queue[k] = key;
    //设置 key 节点的 index 为 k
    setIndex(key, k);
}

        这段代码跟前面入队的逻辑其实很像,相信看懂了入队的逻辑再看这段代码也不在话下了。我们现在讲了入队和出队,还有一个删除没说,但是前面两个只要看懂,删除就完全不在话下了,我把代码贴出来,大家自己分析一下吧~

public boolean remove(Object x) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        int i = indexOf(x);
        if (i < 0)
            return false;

        setIndex(queue[i], -1);
        int s = --size;
        RunnableScheduledFuture<?> replacement = queue[s];
        queue[s] = null;
        if (s != i) {
            siftDown(i, replacement);
            if (queue[i] == replacement)
                // 核心还是在这里
                siftUp(i, replacement);
        }
        return true;
    } finally {
        lock.unlock();
    }
}

ScheduledFutureTask

        上面我们知道了 DelayedWorkQueue 到底是个什么玩意,它内部维护了一个数组 queue 来维护和操作数据,大家还记得这个数组是什么类型的吗?它是 RunnableScheduledFuture 类型的,而 ScheduledThreadPoolExecutor 就有一个 ScheduledFutureTask 内部类,它是 RunnableScheduledFuture 的一个具体实现。那么我们现在大概也能猜出来 ScheduledThreadPoolExecutor 就是依靠 DelayedWorkQueue 和 ScheduledFutureTask 才来达到周期性执行任务的目的的,DelayedWorkQueue 已经分析过了,所以我们这里就分析下 ScheduledFutureTask。

private class ScheduledFutureTask<V>
            extends FutureTask<V> implements RunnableScheduledFuture<V> {

        /** Sequence number to break ties FIFO */
        private final long sequenceNumber;

        // 设定任务开始执行的时间
        private long time;

        // 任务之间的间隔
        private final long period;

        // 通过该变量将任务重新入队
        RunnableScheduledFuture<V> outerTask = this;

        // 分析队列的时候已经分析过了,这个就是对应队列中数组的下标,如果为 -1 代表该任务已经无效了
        int heapIndex;

        // 下面几个是构造函数
        ScheduledFutureTask(Runnable r, V result, long ns) {
            super(r, result);
            this.time = ns;
            this.period = 0;
            this.sequenceNumber = sequencer.getAndIncrement();
        }
        ScheduledFutureTask(Runnable r, V result, long ns, long period) {
            super(r, result);
            this.time = ns;
            this.period = period;
            this.sequenceNumber = sequencer.getAndIncrement();
        }
        ScheduledFutureTask(Callable<V> callable, long ns) {
            super(callable);
            this.time = ns;
            this.period = 0;
            this.sequenceNumber = sequencer.getAndIncrement();
        }

        // 获取任务延迟的时间
        public long getDelay(TimeUnit unit) {
            return unit.convert(time - now(), NANOSECONDS);
        }

        // 上面分析队列的时候也同样分析了,这里主要是队列入队的时候排序用的,这里主要是按照时间进行排序,如果时间比较大,说明要靠后执行,那么就放入队列的后端;相反说明任务需要先执行,放到队列的前端
        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;
        }

        // 判断是否要周期性的执行任务,
        public boolean isPeriodic() {
            return period != 0;
        }

        // 任务执行完成之后,如果是周期性的执行,那么需要设置下一次什么时间执行
        private void setNextRunTime() {
            long p = period;
            if (p > 0)
                time += p;
            else
                time = triggerTime(-p);
        }

        // 取消任务的执行
        public boolean cancel(boolean mayInterruptIfRunning) {
            boolean cancelled = super.cancel(mayInterruptIfRunning);
            if (cancelled && removeOnCancel && heapIndex >= 0)
                remove(this);
            return cancelled;
        }

        // 因为继承了 Runnable 接口,所以重写了 run 方法
        public void run() {
            boolean periodic = isPeriodic();
            if (!canRunInCurrentRunState(periodic))
                cancel(false);
            else if (!periodic)
                ScheduledFutureTask.super.run();
            else if (ScheduledFutureTask.super.runAndReset()) {
                setNextRunTime();
                reExecutePeriodic(outerTask);
            }
        }
    }

        上面的代码量不多,也都做了简单的注释,但是如果有不理解的地方也没有关系,下面会结合 ScheduledFutureTask 和 DelayedWorkQueue 看下 ScheduledThreadPoolExecutor 到底是如何工作的?

ScheduledThreadPoolExecutor 到底是如何实现周期性执行任务的?

        我们前面分析了,ScheduledThreadPoolExecutor 的使用是很简答的,主要逻辑就只有两步,一是调用构造函数实例化一个线程池对象,第二步就是调用方法提交任务。第一步我们分析过了,实例化之后就可以认为获取到了一个线程池,且该线程池是依赖 DelayedWorkQueue 来接收任务的。我们在本小结就分析下第二步,看看任务是怎么提交的,以及怎么周期性执行的。因为 scheduleAtFixedRate 方法和 scheduleWithFixedDelay 方法原理是一样的,所以我这里只分析 scheduleAtFixedRate 方法。

// 调用 executorService.scheduleAtFixedRate(myTask, 1000, 5000, TimeUnit.MILLISECONDS); 会走到该方法
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                              long initialDelay,
                                              long period,
                                              TimeUnit unit) {
    // 如果缺少必要的参数,则直接抛出空指针异常
    if (command == null || unit == null)
        throw new NullPointerException();
    // 如果传递的参数不符合规范,也直接抛出对应的异常
    if (period <= 0)
        throw new IllegalArgumentException();
    // 这里将传递进来的 Runnable 实例进行包装,大家可以看下他的调用链,首先会把 Runnable 包装成  RunnableAdapter 类型的实例,然后通过一个属性 callable 指向该实例。从而实现通过 callable 间接调用我们自定义的 Runnable 任务的 run 方法。
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                      null,
                                      triggerTime(initialDelay, unit),
                                      unit.toNanos(period));
    // 默认这一步没有做什么,返回值还是 sft, 相当于把 ScheduledFutureTask 实例赋值给 t
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    // 这里算是自己通过一个变量引用自己了,可以认为是拥有两个节点的死循环链表
    sft.outerTask = t;
    // 这里将封装成 ScheduledFutureTask 类型的任务提交
    delayedExecute(t);
    return t;
}
// scheduleAtFixedRate 方法总结: 将提交的任务封装成 ScheduledFutureTask 类型的任务,然后通过 outerTask 变量保持自己到自己的引用,为什么要这样下面再分析

private void delayedExecute(RunnableScheduledFuture<?> task) {
    if (isShutdown())
        // 如果线程池已处于非正常运行状态,则拒绝任务的接收
        reject(task);
    else {
        // 这里把任务放入队列
        super.getQueue().add(task);
        // 这个 if 也是判断一些特殊情况,暂时先不管了吧
        if (isShutdown() &&
            !canRunInCurrentRunState(task.isPeriodic()) &&
            remove(task))
            task.cancel(false);
        else
            // 正常情况下会走进这个方法
            ensurePrestart();
    }
}
// delayedExecute 方法总结: 正常流程下该方法把 task 放入队列,然后执行 ensurePrestart 方法


void ensurePrestart() {
    // 获取当前线程池中 Worker 的数量
    int wc = workerCountOf(ctl.get());
    if (wc < corePoolSize)
        addWorker(null, true);
    else if (wc == 0)
        addWorker(null, false);
}
// ensurePrestart 方法总结,该方法就是拿到当前线程池中的 Worker 数量,然后再调用 addWorker()方法
// ensurePrestart 是 ThreadPoolExecutor 的方法其中 Worker 和 ctl 也是 ThreadPoolExecutor 的概念,这也就是为什么我在本文前篇就强调一定要看 ThreadPoolExecutor 那篇文章的原因
//这里再简单介绍一下 addWorker 方法吧,他就是实例化一个 Worker 对象,然后放到对应的队列里去保存起来,一般情况下只会保存 corePoolSize 个 Worker 对象,同时因为 Worker 也是一个 Runnable 类型的对象,线程池实例化出来一个 Worker 之后,就会新建一个线程来执行 Worker 的 run 方法。
//那么回到这里 ensurePrestart 的方法,它就是保证哪怕 corePoolSize 被配置了 0,也要保证当线程池提交任务了之后要有一个后台线程被创建了出来。因为 addWorker 的第一个参数是 null,这个参数代表的是提交的任务,因为这时为 null,说明 addWorker 方法创建线程之后也不会立刻执行任务。

//最后一定要看下ScheduledFutureTask构造函数。留意下各个变量都是什么,包括父类也要看,很简单又很重要
ScheduledFutureTask(Runnable r, V result, long ns, long period) {
    super(r, result);
    this.time = ns;
    this.period = period;
    this.sequenceNumber = sequencer.getAndIncrement();
}



        总结一下上面的流程,当利用线程池提交了一个我们自定义的 Runnable 任务之后,ScheduledThreadPoolExecutor 会先将该任务包装成 ScheduledFutureTask 类型的任务,然后会通过 ScheduledFutureTask 的 outerTask 构建一个自己指向自己的引用,紧接着将该任务放入 DelayedWorkQueue 中保存,最后会判断下当先线程池中没有 Worker 对象或者数量还没有达到 corePoolSize 个,就会通过 addWorker 方法实例化一个 Worker 对象,新建一个线程,但是因为 addWorker 并没有把刚刚实例化的 ScheduledFutureTask 任务提交上来,所以并不会立刻执行该任务。

        至此为止,任务就提交结束了,但是我们也并没有看到任务怎么执行的,更不用说任务怎么周期执行的了。这里就还是结合 ThreadPoolExecutor 那篇文章来分析,在那篇文章里我们知道 Worker 开始工作之后就会陷入死循环,他会一直从队列里获取任务来执行。因为之前分析过,所以我们这里只看下它到底是如何从队列获取任务的。

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        //.. 省略部分跟本篇文章不是很有关联的代码

        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

        可以看到这里会根据是否配置了超时机制而选择调用 workQueue 的 take 或者 poll 方法。这里的 workQueue 就是我们上面分析的 DelayedWorkQueue,它的午餐 poll 方法我已经分析过了,这个有参的跟那个大差不差,大家自己去分析吧,我这里这里就分析下 take 方法,所以此时就要回到 DelayedWorkQueue.take()

public RunnableScheduledFuture<?> take() throws InterruptedException {
      final ReentrantLock lock = this.lock;
      lock.lockInterruptibly();
      try {
          for (;;) {
              // 获取队头元素
              RunnableScheduledFuture<?> first = queue[0];
              // 如果当前队列没有任务,则陷入等待
              if (first == null)
                  available.await();
              else {
                  // 这里调用的是 ScheduledFutureTask 的 getDelay 方法,获取到任务需要延期多久才可以执行
                  long delay = first.getDelay(NANOSECONDS);
                  // 如果延期时间配置 <= 0,那么就会直接返回该任务交由 Woker 执行
                  if (delay <= 0)
                      return finishPoll(first);
                  // 程序执行到这里说明需要延期等待,这时可以移除掉 first的引用,等会再重新获取
                  first = null;
                  // 说明 leader 线程正在工作,当前线程稍微等一等
                  if (leader != null)
                      available.await();
                  else {
                      Thread thisThread = Thread.currentThread();
                      leader = thisThread;
                      try {
                          // 当前线程获得执行权,等待 delay 的时间
                          available.awaitNanos(delay);
                      } finally {
                          if (leader == thisThread)
                              leader = null;
                      }
                  }
              }
          }
      } finally {
          if (leader == null && queue[0] != null)
              available.signal();
          lock.unlock();
      }
  }


// 获取需要延迟多久
public long getDelay(TimeUnit unit) {
    // time 设定的是任务开始执行的时间,再减去当前的时间,就是需要延迟阻塞的时间
    return unit.convert(time - now(), NANOSECONDS);
}

        看到这里我们知道了,从队列中获取到任务之后,会先看下它有没有配置延迟时间,我们也看到了 getDelay 方法的机制。如果提交任务时配置了initialDelay,那么第一次执行就需要延迟 initialDelay。那么怎么周期执行的呢,首先我们知道,现在 Worker 已经从队列中拿到任务了,那么肯定就要调用任务的 run 方法了,所以这个时候我们应该去看下 ScheduledFutureTask 的 run 方法。

public void run() {
    // 只要配置了 period 这里就会返回 true
    boolean periodic = isPeriodic();
    if (!canRunInCurrentRunState(periodic))
        //异常处理
        cancel(false);
    else if (!periodic)
        // 不是周期的话,就执行一次就好了
        ScheduledFutureTask.super.run();
    else if (ScheduledFutureTask.super.runAndReset()) {
        setNextRunTime();
        reExecutePeriodic(outerTask);
    }
}

// run 方法总结,因为我们主要就是探究周期性任务是怎么运行的,所以肯定要重点关注最后一个 if 语句,首先看下 runAndReset()

protected boolean runAndReset() {
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return false;
    boolean ran = false;
    int s = state;
    try {
        // 这个就是在构造函数过程中对我们自定义的 Runnable 任务封装得到的 Callable 实例
        Callable<V> c = callable;
        if (c != null && s == NEW) {
            try {
                // 通过 callable 来出发我们自己编写的 Runnable 任务
                c.call(); // don't set result
                ran = true;
            } catch (Throwable ex) {
                setException(ex);
            }
        }
    } finally {
        runner = null;
        s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
    return ran && s == NEW;
}

// runAndReset方法总结,该方法主要是通过属性 callable 来间接触发我们自定义 Runable 任务的执行,至于 callable 是怎么来的,可以沿着实例化 ScheduledFutureTask 的构造函数跟一下,前面也简单分析过,其实很简单的。
//但是目前为止只是触发了任务的一次执行,再回头看下上面的 if 语句
else if (ScheduledFutureTask.super.runAndReset()) {
    setNextRunTime();
    reExecutePeriodic(outerTask);
}

当 runAndReset 执行完毕后会执行 setNextRunTime 和 reExecutePeriodic,分别看一下

private void setNextRunTime() {
    long p = period;
    if (p > 0)
        time += p;
    else
        time = triggerTime(-p);
}
//setNextRunTime 总结:该方法很简单,只是该任务下次执行的时间,即在上一次执行的时间基础上再加上 period。这样的话当 Woker 从队列中获取到任务之后,就会通过 time 和当前时间再次确定需要阻塞的时间

//注意这里传递来的是前面一直提到的 outerTask 属性
void reExecutePeriodic(RunnableScheduledFuture<?> task) {
    if (canRunInCurrentRunState(true)) {
        // 这里把 task 又放入了队列
        super.getQueue().add(task);
        if (!canRunInCurrentRunState(true) && remove(task))
            task.cancel(false);
        else
            // 这个前面分析过了
            ensurePrestart();
    }
}

        这个 run 方法可以说就是周期执行任务的关键了,它的核心思想就是执行完当前任务之后,通过之前维护的 outerTask(其实就是它自己)再次放入队列中,并且根据 period 和当前时间计算出任务下次执行的时间,那么当 worker 再次取到任务之后就可以通过 getDelay 方法计算出需要等待的时间,如此一来,周期性执行任务的功能就完成了。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值