知识图谱整理之Java基础ScheduledThreadPoolExecutor

前言

之前主要是想了解Spring下的定时任务机制,但是在看了相关源码后,发现必须先要了解ScheduledThreadPoolExecutor,之后在阅读会更加简单,顾先去看了下这块的源码,发现其还是很有意思和学习的地方的。

ScheduledThreadPoolExecutor介绍

我们首先要知道ScheduledThreadPoolExecutor是JUC包下关于定时任务这块的,不知道大家跟我之前有没有一样的疑问,定时任务到底是如何执行的,在ScheduledThreadPoolExecutor就能满足你这个愿望,了解其运作原理。

源码解析

构造方法

这里的构造方法有4种,我拿一个最全的来距离:

public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory,
                                       RejectedExecutionHandler handler) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory, handler);
    }
  • 可以看到构造函数的入参是只能传入核心线程数,线程工厂类和拒绝策略。在之前的文章中我们已经分析了ThreadPoolExecutor,而ScheduledThreadPoolExecutor是继承了这个类的,所以不去过多解释参数作用。
  • 我们发现最大线程数设置的是最大的,其实这里这个参数是无用的,因为我们知道如果阻塞队列是无界的话,那么这个最大线程数参数就是无效的,而这DelayedWorkQueue队列就是这么一个无界队列。

DelayedWorkQueue类源码

首先这个类是是ScheduledThreadPoolExecutor的内部类,它的数据结构是小顶堆,小顶堆是什么?

最小堆,是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于其左子节点和右子节点的值。

怎么理解呢?说直白点就是一颗满二叉树,然后高度越高,值越小。
然后是为什么要这么设计呢?因为是定时任务呀,肯定是现在最短时间执行的排在越上面来进行读取。这边就大致讲一下,如果有问题的,欢迎交流。

下面直接来看源码,我们也知道阻塞队列在ThreadPoolExecutor的运作过程中会涉及到的是take和epoll方法,还有一个就是offer方法,我们就从这些方法入手。

offer方法源码
        public boolean offer(Runnable x) {
            if (x == null)
                throw new NullPointerException();
            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) {
                    queue[0] = e;
                    setIndex(e, 0);
                } else {
                    siftUp(i, e);
                }
                if (queue[0] == e) {
                    leader = null;
                    available.signal();
                }
            } finally {
                lock.unlock();
            }
            return true;
        }
  • grow是扩容方法,比较的简单,这里不过多介绍,有兴趣一看就会
  • setIndex方法设计到ScheduledFutureTask,这个后面会讲,就是把队列的索引序号冗余过去,方便删除的时候能不需要定位查询再删除
  • siftUp就是插入值了,我们之后来看下源码
siftUp方法源码
        private void siftUp(int k, RunnableScheduledFuture<?> key) {
            while (k > 0) {
                int parent = (k - 1) >>> 1;
                RunnableScheduledFuture<?> e = queue[parent];
                if (key.compareTo(e) >= 0)
                    break;
                queue[k] = e;
                setIndex(e, k);
                k = parent;
            }
            queue[k] = key;
            setIndex(key, k);
        }
  • 你品一下,再细品一下,其中(k - 1) >>> 1等于(k-1)/2,因为是小顶堆的数据结构,所以这个值等于k的父节点。
  • 不知道你品出来没有,我这简单说下,就是如果是比父节点大的,就直接在指定位置插入,如果不是,就无限循环,把父节点的位置赋值到原先位置,然后向上查找。
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 {
                        long delay = first.getDelay(NANOSECONDS);
                        if (delay <= 0)
                            return finishPoll(first);
                        first = null; // don't retain ref while waiting
                        if (leader != null)
                            available.await();
                        else {
                            Thread thisThread = Thread.currentThread();
                            leader = thisThread;
                            try {
                                available.awaitNanos(delay);
                            } finally {
                                if (leader == thisThread)
                                    leader = null;
                            }
                        }
                    }
                }
            } finally {
                if (leader == null && queue[0] != null)
                    available.signal();
                lock.unlock();
            }
        }
  • 之前也没有接触过阻塞队列是如何实现的,看完这段代码之后有了更深的了解
  • 首先会获取队列中的第一个元素,如果是0的话,就会执行available.await()进行阻塞,等待调用 available.signal()方法
  • OK,下面就是精髓了,也是我悟了一会才出来的精髓,这里和之前的阻塞队列不同,这里如果有值得话,会执行first.getDelay方法,这个作用是获取这个任务还需要多少时间延迟之后才能执行,如果<=0,则说明可以立即执行了,那么会调用finishPoll进行一些操作后,把队头元素返回。
  • 如果还在延迟的话,注意,后面是精髓,这时leader是为null的,这个leader是干嘛的?我们之后就是来讲这个leader参数的作用。我们可以看到先是available.awaitNanos(delay)来等待对应的延迟时间,所以正常情况在等待了delay醒来之后,会把leader再置为null,再循环执行的时候就可以直接取出返回了。
  • 这里我的说明不知道能不能理解,不理解的话欢迎交流下哦。
  • epoll方法其实和take差不多,这里不做过多介绍,大家理解下take的情况下epoll也很好理解了。

ScheduledThreadPoolExecutor源码解析

我们知道这是继承ThreadPoolExecutor的,顾其使用方式也类似,我们来看下它的execute方法源码

execute方法源码
    public void execute(Runnable command) {
        schedule(command, 0, NANOSECONDS);
    }
  • 实际调用的是schedule源码
schedule方法源码
    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay,
                                       TimeUnit unit) {
        if (command == null || unit == null)
            throw new NullPointerException();
        RunnableScheduledFuture<?> t = decorateTask(command,
            new ScheduledFutureTask<Void>(command, null,
                                          triggerTime(delay, unit)));
        delayedExecute(t);
        return t;
    }
  • decorateTask方法就是第二个传入参数,其中ScheduledFutureTask我们之后分析
  • triggerTime就是获取下次执行的时间,return triggerTime(unit.toNanos((delay < 0) ? 0 : delay));
  • delayedExecute则是实际添加运行
delayedExecute方法源码
    private void delayedExecute(RunnableScheduledFuture<?> task) {
        if (isShutdown())
            reject(task);
        else {
            super.getQueue().add(task);
            if (isShutdown() &&
                !canRunInCurrentRunState(task.isPeriodic()) &&
                remove(task))
                task.cancel(false);
            else
                ensurePrestart();
        }
    }
  • 检查线程池状态,如果是shutdown就直接拒绝
  • 队列添加元素,然后会再确认一遍线程池状态再调用ensurePrestart方法
ensurePrestart方法源码
    void ensurePrestart() {
        int wc = workerCountOf(ctl.get());
        if (wc < corePoolSize)
            addWorker(null, true);
        else if (wc == 0)
            addWorker(null, false);
    }
  • 这里的意思是如果小于核心线程池时或者线程池设置为0,但是还是最少会有一条线程去执行逻辑

ScheduledFutureTask源码解析

这个类呢主要是用来实现延迟任务的,来看下继承图:
继承图

这里可以看到实现的三个方向是Runable、Future、Delayed,就能大概理解了他的功能。我们主要来看下关于Runable的实现,其他两个功能的实现也不难,有兴趣可以自己看下,有问题的话欢迎交流。

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的父类的run和runAndReset方法有点好奇,主要区别是是否吧result赋值进去
  • setNextRunTime会根据time来计算下一次延迟执行的时间
  • 这里简单总结下就是如果是不是周期性运行的,直接调用父类的run方法,然后可以通过get方法获取result值,如果是周期性的,会先计算下次执行时间, 然后再次把任务放入队列中。

个人总结

这篇定时任务总结的话,主要还是想要理清定时延迟任务时如何实现的,我们从头来开始理一下,首先是通过定义一个正常的线程池,这里的重点区别是放入的阻塞队列是内部类DelayedWorkQueue,然后回忆下这个类用到的offer、take、poll方法内容。OK,在使用中我们调用execute方法,实际上是封装成了一个内部的ScheduledFutureTask方法,然后通过其实现周期性运行。延迟的话也是通过ScheduledFutureTask实现了Delayed可获取到还需延迟的时间。

今日的知识图谱:
知识图谱

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值