Java8之ScheduledThreadPoolExecutor实现原理

ScheduledThreadPoolExecutor是一个可实现定时任务的线程池,ScheduledThreadPoolExecutor内的任务既可以在设定的时间到达时执行一次,也可以相隔固定时间周期执行。

ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,关于ThreadPoolExecutor的原理可参考:

Java8之ThreadPoolExecutor实现原理

ScheduledThreadPoolExecutor相比较于ThreadPoolExecutor,只多了四个提交任务的方法,这四个方法实现自ScheduledExecutorService,继承自ThreadPoolExecutor的execute和submit方法都调用了自己的schedule方法

  1. 第一个schedule的任务是runnable,不支持获取返回值,会在指定延迟时间之后执行一次
  2. 第二个schedule的任务是callable,可获取返回值,会在指定延迟时间之后执行一次
  3. scheduleAtFixedRate的任务为runnable,不支持获取返回值,会在指定延迟时间后执行一次,之后按设定好的间隔时间重复执行
  4. scheduleWithFixedDelay的任务为runnable,不支持获取返回值,会在指定延迟时间后执行一次,并在执行完成时间的基础上按间隔时间周期执行

后两个方法可能理解起来不是很清楚,简单点说,scheduleAtFixedRate的下一次任务的执行时间为:法定的任务开始时间(并不是任务真正开始执行的时间,因为线程池是在任务执行完成后再计算下一次的开始时间,此时计算出的开始时间可能已经小于现在的时间了,当这个任务执行的时候,真实的执行时间肯定比设定好的时间大)+间隔;scheduleWithFixedDelay的下一次任务执行时间为:任务结束时间+间隔。

我们将scheduleAtFixedRate演示一下,正常情况如下,其中0、5、15、20就是法定的任务开始时间

Java8之ScheduledThreadPoolExecutor实现原理_第1张图片

不正常的情况如下

Java8之ScheduledThreadPoolExecutor实现原理_第2张图片

实现原理

ScheduledThreadPoolExecutor之所以能够实现上述的定时功能,本质上是通过两点实现的:

1.将任务包装在ScheduledFutureTask内。ScheduledFutureTask相当于原始任务的代理,在执行原始任务之后会视情况将任务的执行时间修改后再加入工作队列

2.使用了DelayedWorkQueue作为任务队列。DelayedWorkQueue内部是一个ScheduledFutureTask的数组,queue的功能类似于DelayQueue,在设定的时间到达时才能取出队列中的元素。

下面我们基于以上两点来分析源码。

1.构造方法

以最后一个构造函数为例,四个构造方法都调用了父类(ThreadPoolExecutor)的构造方法,最大线程数都设置为int最大值,线程存活时间都为0,工作队列都为DelayedWorkQueue

    public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory,
                                       RejectedExecutionHandler handler) {
        //最大线程数为int最大值,线程空转时间为0,使用DelayedWorkQueue作为工作队列
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory, handler);
    }

2.任务提交

普通的schedule方法会将间隔时间设为0,只执行一次;scheduleAtFixedRate会将时间间隔设为正数;scheduleWithFixedDelay会将时间间隔设为负数

    public ScheduledFuture schedule(Runnable command,
                                       long delay,
                                       TimeUnit unit) {
        if (command == null || unit == null)
            throw new NullPointerException();
        //将任务包装成ScheduledFutureTask。
        //ScheduledFutureTask的构造方法内会将间隔时间设为0
        //ScheduledFutureTask的time会设为计算出的任务执行时间
        RunnableScheduledFuture t = decorateTask(command,
            new ScheduledFutureTask(command, null,
                                          triggerTime(delay, unit)));
        //判断线程池状态,一切正常会将任务加入工作队列
        delayedExecute(t);
        return t;
    }


    public  ScheduledFuture schedule(Callable callable,
                                           long delay,
                                           TimeUnit unit) {
        if (callable == null || unit == null)
            throw new NullPointerException();
        //将任务包装成ScheduledFutureTask。
        //ScheduledFutureTask的构造方法内会将间隔时间设为0
        //ScheduledFutureTask的time会设为计算出的任务执行时间
        RunnableScheduledFuture t = decorateTask(callable,
            new ScheduledFutureTask(callable,
                                       triggerTime(delay, unit)));
        //判断线程池状态,一切正常会将任务加入工作队列      
        delayedExecute(t);
        return t;
    }


    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。
        //间隔时间设为相应的时间单位的值,值为正数
        //ScheduledFutureTask的time会设为计算出的任务执行时间
        ScheduledFutureTask sft =
            new ScheduledFutureTask(command,
                                          null,
                                          triggerTime(initialDelay, unit),
                                          unit.toNanos(period));
        RunnableScheduledFuture t = decorateTask(command, sft);
        //将任务指向自己,将任务再放回队列时会用到该值
        sft.outerTask = t;
        //判断线程池状态,一切正常会将任务加入工作队列
        delayedExecute(t);
        return t;
    }


    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。
        //间隔时间设为相应的时间单位的值,值为负数
        //ScheduledFutureTask的time会设为计算出的任务执行时间
        ScheduledFutureTask sft =
            new ScheduledFutureTask(command,
                                          null,
                                          triggerTime(initialDelay, unit),
                                          unit.toNanos(-delay));
        RunnableScheduledFuture t = decorateTask(command, set);
        //将任务指向自己,将任务再放回队列时会用到该值
        sft.outerTask = t;
        //判断线程池状态,一切正常会将任务加入工作队列
        delayedExecute(t);
        return t;
    }
    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();
        }
    }

3.ScheduledFutureTask

1.参数

Java8之ScheduledThreadPoolExecutor实现原理_第3张图片

  1. sequenceNumber用于当两个任务时间相同时,sequenceNumber小的排在工作队列的前边
  2. time记录了任务的开始时间
  3. period记录了任务的执行间隔时间
  4. outerTask是一个指向自己的引用,当任务要重复执行时,就是将这个引用再放入工作队列
  5. heapIndex记录了任务在队列里的坐标,方便快速取消任务

2.run方法

ScheduledFutureTask是一个runnable,run是他的核心方法

    public void run() {
        //根据间隔时间判断是不是需要多次执行的任务
        boolean periodic = isPeriodic();
        //判断当前任务在线程池SHUTDOWN后需不需要执行
        if (!canRunInCurrentRunState(periodic))
            cancel(false);
        //如果是一次性的任务,执行该任务
        else if (!periodic)
            ScheduledFutureTask.super.run();
        //如果是需要多次执行的任务,执行该任务
        else if (ScheduledFutureTask.super.runAndReset()) {
            //设置该任务下次执行的时间
            setNextRunTime();
            //将修改好时间的任务再次放入工作队列
            reExecutePeriodic(outerTask);
        }
    }

isPeriodic方法很简单, 根据间隔时间判断

    public boolean isPeriodic() {
        //如果间隔时间为0,则是一次性任务
        return period != 0;
    }

setNextRunTime是设置下一次执行时间的方法

    private void setNextRunTime() {
        //提交任务时,如果使用的scheduleWithFixedDelay方法,会将间隔时间保存为负数
        long p = period;
        //如果间隔时间>0,将下次执行时间设置为:法定执行时间+执行间隔
        if (p > 0)
            time += p;
        else
        //将下次执行时间设置为:当前时间+执行间隔
            time = triggerTime(-p);
    }

ScheduledThreadPoolExecutor的reExecutePeriodic方法将任务重新放回队列

    void reExecutePeriodic(RunnableScheduledFuture task) {
        //判断线程池当前状态能否继续执行任务
        if (canRunInCurrentRunState(true)) {
            //将任务加入队列,DelayedWorkQueue会按时间和序列号将任务插到合适的位置
            super.getQueue().add(task);
            //再次判断线程池状态,防止状态已修改
            if (!canRunInCurrentRunState(true) && remove(task))
                task.cancel(false);
            else
                //此方法会将工作线程数量补充到设置的核心线程数量
                //如果设置的核心线程数为0,则会添加一个普通工作线程
                ensurePrestart();
        }
    }

在将ScheduledFutureTask放入任务队列后,队列会根据ScheduledFutureTask的compareTo方法将任务排序

    public int compareTo(Delayed other) {
        if (other == this) 
            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;
    }

4.DelayedWorkQueue

该阻塞队列是使用数组存储任务

private RunnableScheduledFuture[] queue =
            new RunnableScheduledFuture[INITIAL_CAPACITY];

存方法,存入的任务在任务队列内只能保证一个粗略有序

        //调用线程池的提交任务方法时内部调用的就是队列的add方法
        public boolean add(Runnable e) {
            //add方法将任务委托给offer方法
            return offer(e);
        }
        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;
                //如果队列存储的数据达到了队列的容量,将队列进行扩容
                //扩容后容量 = 扩容前容量 + 扩容前容量*0.5
                //如果扩容后容量超过int最大值,则将容量设置为int最大值
                if (i >= queue.length)
                    grow();
                size = i + 1;
                //如果i==0,说明这是第一个任务,直接放在下标0的位置
                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;
        }
        //二分插入方法
        private void siftUp(int k, RunnableScheduledFuture key) {
            while (k > 0) {
                //右移1位,找到k中间的下标
                int parent = (k - 1) >>> 1;
                RunnableScheduledFuture e = queue[parent];
                //如果要插入的任务大于等于下标位置,则跳出循环,任务会插在k的位置
                if (key.compareTo(e) >= 0)
                    break;
                //如果要插入的任务小于下标位置,则将该下标内的位置换到k,并将k赋值为中间下标
                queue[k] = e;
                setIndex(e, k);
                k = parent;
            }
            queue[k] = key;
            setIndex(key, k);
        }

取方法分三种,poll获取不到立刻返回null,take会阻塞获取,poll会在超时时间内阻塞获取,以take为例

        public RunnableScheduledFuture take() throws InterruptedException {
            final ReentrantLock lock = this.lock;
            //事物类操作都要先获取锁,此处加的是可响应中断的锁
            lock.lockInterruptibly();
            try {
                for (;;) {
                    RunnableScheduledFuture first = queue[0];
                    //如果队列内没有任务,则在lock的等待队列上等待
                    if (first == null)
                        available.await();
                    else {    
                        //获取队列上第一个任务还需等待的时间
                        long delay = first.getDelay(NANOSECONDS);
                        //如果第一个任务的执行时间已经到了,则取出该任务
                        if (delay <= 0)
                            return finishPoll(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();
            }
        }

 

你可能感兴趣的:(java)