Java定时任务(2)

Java定时任务(2)

上次浅显的分析了Timer及TimerTask的调度原理,这里我们再来看一下另一种定时调度方式ScheduledThreadPoolExecutor的内部执行原理。

  • ScheduledThreadPoolExecutor调度方式
  • 阻塞队列DelayedWorkQueue的实现
  • ScheduledFutureTask控制调度过程
  • 总结

ScheduledThreadPoolExecutor调度方式

类似于Timer,它的调度方式也有4种,我们抽出其中一种来具体分析。

//一次性任务,无返回值
public ScheduledFuture<?> schedule(Runnable command,
                                       long delay,
                                       TimeUnit unit) {...}

//一次性任务,有返回值
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                           long delay,
                                           TimeUnit unit) {...}


public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit) {...}


public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit){...}

我们取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;
    }

//将Runnable适配成FutureTask
ScheduledFutureTask<Void> sft =
            new ScheduledFutureTask<Void>(command,
                                          null,
                                          triggerTime(initialDelay, unit),
                                          unit.toNanos(-delay));

//由于是将一个无返回值的Runnable包装成一个FutureTask,所以传入一个null作为返回值
 ScheduledFutureTask(Runnable r, V result, long ns, long period) {
            super(r, result);
            this.time = ns;         //设置执行时间
            this.period = period;   //设置时间片
            this.sequenceNumber = sequencer.getAndIncrement();
        }

//Executors.callable()是真正进行Runnable与FutureTask适配的函数
public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

public static <T> Callable<T> callable(Runnable task, T result) {
        if (task == null)
            throw new NullPointerException();
        return new RunnableAdapter<T>(task, result);
    }    

//call()就不多做介绍了,由FutureTask的call来掉内部Runnable的run()
static final class RunnableAdapter<T> implements Callable<T> {
        final Runnable task;
        final T result;
        RunnableAdapter(Runnable task, T result) {
            this.task = task;
            this.result = result;
        }
        public T call() {
            task.run();
            return result;
        }
    }

    //修改或替换用于执行一个可运行的任务。
    //这个方法可以用来覆盖具体类以管理内部任务。
    //默认实现仅返回给定的任务。
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);

    protected <V> RunnableScheduledFuture<V> decorateTask(
        Runnable runnable, RunnableScheduledFuture<V> task) {
        return task;
    }


//这里之所以先将task放入队列,再由worker去取出并运行,是因为task有可能并不期望当下就被运行,而是期望一段时间的延迟后再开始运行。
//同时一个task被创建后直接运行,也是不合理的一种行为。
private void delayedExecute(RunnableScheduledFuture<?> task) {
        if (isShutdown())       //线程池状态是否正常
            reject(task);       //拒绝任务,根据拒绝策略,通常有1.直接丢弃 2.抛异常 3.新建一个线程来执行 4.类似于LRU,丢弃队列中最久的一个任务
        else {
            super.getQueue().add(task);         //先加入BlockingQueue,该队列默认在ThreadPoolExecutor中实现,但这里提供了DelayedWorkQueue代替默认实现
            if (isShutdown() &&
                !canRunInCurrentRunState(task.isPeriodic()) &&      //当task已经被加入队列,但是线程池挂了,根据status来决定,取消并删除在队列中的task。
                remove(task))
                task.cancel(false);
            else
                ensurePrestart();
        }
    }


//其实就是保证至少有一个worker已经启动
    void ensurePrestart() {
        int wc = workerCountOf(ctl.get());
        if (wc < corePoolSize)
            addWorker(null, true);
        else if (wc == 0)
            addWorker(null, false);
    }

至此,一个定时任务就被加入DelayedWorkQueue队列了,而调度任务的线程由ThreadPoolExecutor提供。
但是到此为此还并没有实现延迟调度的功能。
接下来说说DelayedWorkQueue是如何实现延迟调度的。

阻塞队列DelayedWorkQueue的实现

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();     //重建数组,长度增加50%
                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;
        }

//这点跟Timer中的操作比较类似,做了一个相对排序而非绝对意义的排序。
//内部做了一个比较接口,根据下次执行的时间来排队。
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);
        }

//但是显然offer并没有来提供延迟调度的实现,真正的延迟调度的实现是在take中完成的。

 public RunnableScheduledFuture<?> take() throws InterruptedException {
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly();
            try {
                for (;;) {
                    RunnableScheduledFuture<?> first = queue[0];    //第一个元素,也就是优先级最高,最应该被执行的任务
                    if (first == null)
                        available.await();  //进入这里说明队列中没有待执行的任务,所以来取任务的worker进入等待状态
                    else {
                        long delay = first.getDelay(NANOSECONDS);   //取到任务,getDelay定义于内部类ScheduledFutureTask中
                        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,则说明该立即被执行。
//否则说明仍需等待。
 public long getDelay(TimeUnit unit) {
            return unit.convert(time - now(), NANOSECONDS);
        }


//返回第一个task,并将第一个元素与最后一个元素互换位置。
//通常因为我们在offer一个task的时候已经进行过相对排序,使得这个队列在大体上是相对有序的。
//而这里在take走一个元素后却将最后一个元素(理论上来说,无论是优先级是相对低的,下次执行时间都是最久的)互换。
 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;
        }

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];
                if (key.compareTo(c) <= 0)
                    break;
                queue[k] = c;
                setIndex(c, k);
                k = child;
            }
            queue[k] = key;
            setIndex(key, k);
        }

siftDown()大体上是一个自顶向下的一个相对排序。
首先half = size >>> 1 = size/2。
其次,关注whlie(k < half),我们假设永不会走到break,下面我简单的列出部分数组下标变化情况。
假设 size = 32。

k child right
0 1 2
1 3 4
3 7 8
7 15 16
15 31 32

由于条件是k < half
所以至多循环log2(size)次。
类比于siftUp,其实是一次自顶向下的相对排序,结果也会是相对有序,我猜可能是为了弥补siftUp()时的不足,所以再进行了一次相对排序。
至此,已经从大体上了解了DelayedWorkQueue是如何实现延迟调度的。

最后,在来看下worker取到task之后如何run
由于task被ScheduledFutureTask包装,所以我们看的自然是ScheduledFutureTask的run()。

ScheduledFutureTask控制调度过程

public void run() {
        boolean periodic = isPeriodic();    //判断是否是一个循环任务
        if (!canRunInCurrentRunState(periodic))
            cancel(false);
        else if (!periodic)
            ScheduledFutureTask.super.run();    //若不是循环任务,当做一个普通任务来处理
        else if (ScheduledFutureTask.super.runAndReset()) { //若是循环任务,在runAndReset()中实际运行,运行完毕后同时将status重新置为NEW,也就是相当于一个新task
            setNextRunTime();       //计算下次任务执行的时间,这里与Timer有所区别,Timer是先计算出来在运行,而这里是先运行在计算
            reExecutePeriodic(outerTask);       //自然,它完成的就是讲task重新放入队列,并做一些线程池状态的相关判断
        }
    }

总结

1.延迟调度由DelayedWorkQueue来实现。
2.Executors.callable()完成Runnable到Callable的转换(适配)。
3.ScheduledFutureTask包装task,实现循环任务的“循环”处理,延迟计算,调度控制等相关操作。
4.ThreadPoolExecutor实现多线程并发操作。
5.相比于Time,最为明显的几点:
  1.Timer单线程,ScheduledThreadPoolExecutor多线程(显而易见)。
  2.Timer先计算下次调度时间,在运行,ScheduledThreadPoolExecutor相反。
  3.Timer之间无法实现TimerTask共享,ScheduledThreadPoolExecutor通过ThreadPoolExecutor可以实现。
  4.Timer与ScheduledThreadPoolExecutor使用的BlockingQueue不同。

你可能感兴趣的:(java,定时任务)