ScheduledThreadPoolExecutor源码详解

介绍

ScheduledThreadPoolExecutor是JDK提供的一个基于内存的定时任务调度器继承于ThreadPoolExecutor,需要注意的就是scheduleAtFixedRate()方法和scheduleWithFixedDelay()方法的区别。

ThreadPoolExecutor源码详解

详解

这里我们依旧将其分为三个模块:

  1. 调度器
  2. 任务队列
  3. 执行线程

任务队列负责管理用户提交的任务信息,执行线程负责去执行任务,而调度器居中协调和维护整个容器的状态和分配任务的执行线程。

调度器

首先ScheduledThreadPoolExecutor继承自ThreadPoolExecutor
先说结论:ScheduledThreadPoolExecutor相比较于ThreadPoolExecutor,做出了以下的改动:

差异

  • 实现了任务队列(延迟队列) DelayedWorkQueue赋值给ThreadPoolExecutor.workQueue,改变了队列存储和获取的条件
  • 定义了新的Task包装类ScheduledFutureTask,改变了任务运行时的判断条件
  • 重写了任务提交的方式,改变了任务提交行为

除了上述所说的差异点,当然存在着一些相同点。
相同点

  • 执行线程的创建方式
  • 执行线程获取任务的方式
  • 容器的生命周期变化
    首先,本次的源码阅读起点先定在构造方法中(有上一章ThreadPoolExecutor源码详解的基础,不用在从变量或生命周期开始了)

构造方法

    public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory,
                                       RejectedExecutionHandler handler) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
                new DelayedWorkQueue(), threadFactory, handler);
    }

上述构造方法,是入参最多,自定义最多的但是相比于ThreadPoolExecutor动则6,7个还是要少的可怜。

首先允许自定义的有

  • 核心线程数
  • ThreadFactory
  • 拒绝策略

不允许自定义的有

  • 最大线程数
  • 线程等待时间
  • 线程等待时间单位
  • 任务队列(默认为DelayedWorkQueue

任务提交

ThreadPoolExecutor中,我们的提交方式有submit()execute()
ScheduledThreadPoolExecutor中,依然有submit()execute(),但是新增了schedule()scheduleAtFixedRate(),scheduleWithFixedDelay()方法
这几个方法的区别就在于

延迟性任务
submit()execute()schedule()

其中submit()execute()延迟时间默认为0,其实就是立即(底层也是调用的schedule()),schedule()就是设置的延迟时间。

周期性任务
scheduleAtFixedRate()scheduleWithFixedDelay()

scheduleAtFixedRate():固定频率执行
scheduleWithFixedDelay():以任务结束为起点,周期延迟性执行
这样说不太直观,举个例子:
scheduleAtFixedRate() 设置频率为5s,前一个任务在15:50:00 开始执行,然后在15:50:03结束,那么下一次执行时间是在15:50:05。而如果结束时间是在15:50:06,那么任务结束后就会立即开始下一次的任务。
scheduleWithFixedDelay() 设置频率为5s,前一个任务在15:50:00 开始执行,然后在15:50:03结束,那么下一次执行时间是在15:50:08。

这里要先说下ScheduledThreadPoolExecutor中的两个属性:

  • continueExistingPeriodicTasksAfterShutdown,true:表示在SHUTDOWN状态下周期性任务继续执行
  • executeExistingDelayedTasksAfterShutdown,true:表示在SHUTDOWN状态下延迟性任务继续执行

接下来就是看ScheduledThreadPoolExecutor对于task的定义对象ScheduledFutureTask

ScheduledFutureTask

首先是它的成员属性

        //自增序列号,保证FIFO
        private final long sequenceNumber;
        //下一次任务执行的时间
        private long time;
        //周期性执行间隔设置,0代表非周期,非0代表周期
        private final long period;
        RunnableScheduledFuture<V> outerTask = this;
        //在延迟队列中的索引,用于快速从队列中获取
        int heapIndex;

然后是它的执行方法

        public void run() {
        	//是否是周期
            boolean periodic = isPeriodic();
            //SHUTDOWN并且不继续(那两个超级长的变量为false)则取消任务
    		//说人话就是容器未运行,并且SHUTDOWN的时候不允许任务在执行那就取消
            if (!canRunInCurrentRunState(periodic))
                cancel(false);
            //非周期执行
            else if (!periodic)
                ScheduledFutureTask.super.run();
            //周期执行    
            else if (ScheduledFutureTask.super.runAndReset()) {
            	//设置下一次的执行时间
                setNextRunTime();
               	//创建线程
                reExecutePeriodic(outerTask);
            }
        }

简单的说:非周期就执行一次,周期任务就需要计算下次执行的时间

    void reExecutePeriodic(RunnableScheduledFuture<?> task) {
    	//RUNNING 或者 SHUTDOWN并且continueExistingPeriodicTasksAfterShutdown为ture
        if (canRunInCurrentRunState(true)) {
        	//任务加入延迟队列
            super.getQueue().add(task);
            //doubleCheck,防止这个时候容器状态变了。
            //如果容器关闭,并且SHUTDOWN不运行执行,而且还把这个任务从任务队列中移除成功了。
            if (!canRunInCurrentRunState(true) && remove(task))
            	//那我就直接取消任务
                task.cancel(false);
            else
            	//不然就创建个线程与其对应
                ensurePrestart();
        }
    }

执行线程的创建:

    void ensurePrestart() {
        int wc = workerCountOf(ctl.get());
        //如果小于核心线程数,创建核心线程
        if (wc < corePoolSize)
            addWorker(null, true);
        //如果核心线程数设置为0,那么创建普通线程,按照构造参数默认的Integer.MAX_VALUE
        //相当于无限的线程  
        else if (wc == 0)
            addWorker(null, false);
    }

铛铛铛,喜闻乐见的提问时间到

问:是否能保证定时的任务一定会在我们规定的时间启动?
答:假设最大线程允许我们设置,核心线程5,这时候进行执行的定时任务10,那么只有5个线程去执行五个任务,剩下5个任务需要等前面的核心线程执行完了才会来执行,如果执行的慢,那就会出现定时设置的是1点执行,但是可能4,5点才开始跑任务,那就与我们的预期不同。

问:根据问题1是否能表示"最大线程数"没有任何意义?
答:不是,当"核心线程数"为0,那么就会创建创建普通线程与任务一一对应。

问:线程等待时间为什么是0?或者说为什么执行完毕后要立即关闭空闲的普通线程?
答:应该是因为定时任务的特点,会是一波一波的来,那么执行完毕后不关闭意义就不大,但这也面临着一些问题,每一次都可能创建线程然后销毁线程,这明显是比较浪费资源和消耗时间的。

通过上面两个问题已经了解了不允许自定义构造参数中的三个,还差最后一个 任务队列DelayedWorkQueue,这个留在后面在分析。

十万个问什么ing:

问:有20个任务,并且任务的time都是500(虚构),那么任务的执行顺序是什么(任务执行时间一样)?
答:根据sequenceNumber,由小到大,每一个任务都会被分配一个单调递增不重复的sequenceNumber。

问:延迟性的executeExistingDelayedTasksAfterShutdown为true比较好理解,在SHUTDOWN执行完就没有了,但是假设周期性的continueExistingPeriodicTasksAfterShutdown设置为true,容器能停止下来吗?
答:在容器中的周期任务会一直反复执行,容器不接收新的任务,同时也不会停止。(默认为false)

好了,ScheduledThreadPoolExecutor对于自己任务的定义和封装我们理解完了,接下来从最简单的任务提交方法schedule()方法开始,let’s go!!!

schedule()
    public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                           long delay,
                                           TimeUnit unit) {
        if (callable == null || unit == null)
            throw new NullPointerException();
        //通过triggerTime方法计算出下次执行的时间
        //包装成RunnableScheduledFuture
        RunnableScheduledFuture<V> t = decorateTask(callable,
                new ScheduledFutureTask<V>(callable,
                        triggerTime(delay, unit)));
        //延迟执行                
        delayedExecute(t);
        return t;
    }
    private void delayedExecute(RunnableScheduledFuture<?> task) {
    	//如果状态不是RUNNING,直接拒绝
    	//这里不是判断是否是SHUTDOWN状态,而是RUNNING方法名有歧义
        if (isShutdown())
            reject(task);
        else {
        	//向任务队列中添加任务
            super.getQueue().add(task);
            /*
            *如果状态不是RUNNING,
            *或者是SHUTDOWN并且continueExistingPeriodicTasksAfterShutdown(延迟性任务就是executeExistingDelayedTasksAfterShutdown)为true并且移除任务成功
            */
            if (isShutdown() &&
                    !canRunInCurrentRunState(task.isPeriodic()) &&
                    remove(task))
                //取消任务的执行
                task.cancel(false);
            else
            	//创建工作线程
                ensurePrestart();
        }
    }

先梳理先schedule()的流程

非RUNNING
RUNNING
SHUTDOWN并且不运行执行并且移除成功
不满足
计算下次执行时间
容器状态
拒绝任务
任务入列
状态检测
取消任务
创建执行线程

简单理解:状态正常的情况下,每创建一个任务都会与之对应的创建一个线程。

scheduleAtFixedRate()
    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();
            //构造的时候,设置下次执行时间和周期性时间
        ScheduledFutureTask<Void> sft =
                new ScheduledFutureTask<Void>(command,
                        null,
                        triggerTime(initialDelay, unit),
                        unit.toNanos(period));
        RunnableScheduledFuture<Void> t = decorateTask(command, sft);
        sft.outerTask = t;
        delayedExecute(t);
        return t;
    }

解释下几个入参:

  1. command:就是我们用户提交的任务
  2. initialDelay:第一次执行延迟时间
  3. period:周期性间隔时间
  4. unit:间隔时间的单位

第一次的执行延迟时间通过在计算时间中设置来表示。

scheduleAtFixedRate()schedule()区别点就在于:

  • period不同
    scheduleAtFixedRate()将周期性间隔时间存放在ScheduledFutureTaskperiod成员属性中(正数)。
    schedule()ScheduledFutureTaskperiod成员属性默认为0
scheduleWithFixedDelay()
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit) {
        if (command == null || unit == null)
            throw new NullPointerException();
        if (delay <= 0)
            throw new IllegalArgumentException();
        //构造的时候,设置下次执行时间和周期性时间
        ScheduledFutureTask<Void> sft =
                new ScheduledFutureTask<Void>(command,
                        null,
                        triggerTime(initialDelay, unit),
                        unit.toNanos(-delay));
        RunnableScheduledFuture<Void> t = decorateTask(command, sft);
        sft.outerTask = t;
        delayedExecute(t);
        return t;
    }

scheduleWithFixedDelay()scheduleAtFixedRate()区别点就在于:

  • period不同
    scheduleAtFixedRate()ScheduledFutureTaskperiod成员属性为正数。
    scheduleWithFixedDelay()ScheduledFutureTaskperiod成员属性为负数。

源码实现:

        private void setNextRunTime() {
            long p = period;
            //如果为正数代表是scheduleAtFixedRate,那么就在上一次时间+间隔时间
            if (p > 0)
                time += p;
            else
            	//如果为负数代表是scheduleWithFixedDelay,那么就是现在时间+间隔时间
                time = triggerTime(-p);
        }

ok,调度器中任务提交已经阅读完毕,旅途已过半,加油ヾ(◍°∇°◍)ノ゙!

任务队列(延迟队列)

接下来我们就要来看看DelayedWorkQueue任务的延迟队列是如何获取任务的了。

首先还是来看看其中的成员变量

		//任务队列,使用的是数组,需要考虑扩容问题
        private RunnableScheduledFuture<?>[] queue =
                new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
		//首个任务的执行线程
		private Thread leader = null;

需要稍微理解下的就一个leader,这个属性等后面看到相关使用得地方在来聊,这样比较好理解一些。

DelayedWorkQueue中主要看三个方法,入列:offer(),出列:poll()take()

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);
                }
                //如果首个任务被替换成了用户提交的x
                if (queue[0] == e) {
                	//要首个任务占有的线程清空,这里需要结合poll()和take()来看,后续在聊
                    leader = null;
                    //那么要唤醒线程,去尝试获取任务
                    available.signal();
                }
            } finally {
                lock.unlock();
            }
            return true;
        }

接下来就看下排序问题

siftUp()
        private void siftUp(int k, RunnableScheduledFuture<?> key) {
            while (k > 0) {
            	//获取当前索引元素的父节点
                int parent = (k - 1) >>> 1;
                RunnableScheduledFuture<?> e = queue[parent];
                //如果加入的元素大于父节点,那就存放在K中不用动
                if (key.compareTo(e) >= 0)
                    break;
                //如果不是,那么就将父节点的数据和K当前插入的位置进行交换    
                queue[k] = e;
                setIndex(e, k);
                k = parent;
            }
            queue[k] = key;
            setIndex(key, k);
        }

整体思路是这样的,数组是一个二叉树,这棵树呢,有规则,父节点比子节点要小(也叫小顶对),这样的特点也就产生了,数组中的第一个元素一定是最先执行的。

简单想象:siftUp就相当于一个刚加入的元素不停向上比较并交换位置,一直到父节点比它还小为止。

举个例子:

1
3
4
5
8
12

这时候新增一个2,那么现在的索引就是7,根据int parent = (k - 1) >>> 1算出父节点索引为3,也就是数值为4的那个节点,因为2<4所以交换位置,上升一位,由于1<2所以不在上升,停止。

1
2
3
4
5
8
12

问问问

问:为什么排序会存在,直接放数组中不行吗?
答:不行,效率太低,要时时去判断数组中所有任务现在该不该执行,使用小顶堆(二叉树)优化查询效率。

poll()

尝试获取任务:

        public RunnableScheduledFuture<?> poll() {
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                RunnableScheduledFuture<?> first = queue[0];
                //如果队列中没有任务,或者任务的执行时间没到,则返回null
                if (first == null || first.getDelay(NANOSECONDS) > 0)
                    return null;
                else
                    return finishPoll(first);
            } finally {
                lock.unlock();
            }
        }
        private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) {
            int s = --size;
            RunnableScheduledFuture<?> x = queue[s];
            //将任务缩减一个
            queue[s] = null;
            //如果任务队列不为空,那么就进行重排序
            if (s != 0)
                siftDown(0, x);
            setIndex(f, -1);
            return f;
        }
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
                        //如果第一个任务不能执行,并且leader不为空,则等待
                        if (leader != null)
                            available.await();
                        else {
                        	//将leader设置为当前线程
                            Thread thisThread = Thread.currentThread();
                            leader = thisThread;
                            try {
                            	//并等待任务距离执行的间隔时间
                                available.awaitNanos(delay);
                            } finally {
                            	//当唤醒时,需要看下是否leader被切换了,如果没被切换则设置为null
                                if (leader == thisThread)
                                    leader = null;
                            }
                        }
                    }
                }
            } finally {
                if (leader == null && queue[0] != null)
                    available.signal();
                lock.unlock();
            }
        }

到这里延迟队列就大致梳理完毕了,接下来就是收尾看看leader这个属性的作用。

问题是这样的,如果第一个任务被执行线程获取到并且不能执行,那么就会等待 该任务到执行时间的间隔时间 这么长的时间,但是第一个任务只有一个,那到时候唤醒之后大家还要来进行竞争,这就浪费资源了,所以解决方案就是,当一个线程回去到第一个任务,并且发现现在无法执行,那么他就说,这个任务我领了,到时候我醒过来执行,在唤醒你们执行后面的,你们直接等着就行。

文字太过苍白,举例子:

获取到首个任务
到达
还差delay时间
沉睡delay时间
delay时间后
获取到首个任务
Thread1
任务时间
执行任务
设置leader
睡眠
thread3
任务10s后才到执行时间
leader被其他线程占有
一直沉睡等待唤醒

ok,本次旅程已结束,各位乘客请按序下车!(* ̄︶ ̄)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

唐芬奇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值