JDK Concurrent包组件概解(4)- ScheduledThreadPoolExecutor & Timer

 两者都可用来延时或定时执行任务,在实现上它两的设计思路是相似的,细节逻辑上略有差异。

相同

一、

都有一个可变数组队列来保存任务。

将它看做一个完全二叉树结构( x, 2x, 2x+1)队列内不保证所有元素严格有序,但树结构上的每一个三元组<left, parent, right> 里 parent一定是最小的;那么任意子树的root一定是全局最小。

当队列元素发生入队、出队时,都会按“下一个执行时间” 进行堆排序来保证队首元素的延时是最少的。

二、

 在各自封装任务的类结构里,都有一个 long类型属性“period” , 通过它来确定任务下一次执行时间。

  • 0  :  一次性任务,在调用方法时不要指定“period” 。
  • 正数:   更新 nextExecutionTime  =  上一次的nextExecutionTime  +  period 。      scheduleAtFixedRate
  • 负数:   更新 nextExecutionTime  =  now()  - period  。 scheduleWithFixedDelay

不同

一、

  • Timer里使用的是单线程TimerThread阻塞式依次执行,只能捕获并无视InterruptedException ,其他异常会抛出并退出线程,定时器失效。
  • ScheduledThreadPoolExecutor将任务封装成了ScheduledFutureTask,FutureTask的执行方法#run()、#runAndReset()内部会捕获并处理异常,所以不影响线程池内工作线程的运行。

二、

当周期性执行任务时:

  • Timer是在任务执行开始前就计算下次执行时间;
  • ScheduledThreadPoolExecutor因为是线程池的工作模式,所以:任务会先弹出队列,在执行完成后才计算下次执行时间。

三、

Timer可以指定具体的某个执行时间点,而ScheduledThreadPoolExecutor不行。

ScheduledThreadPoolExecutor

 public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) {
     super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory);
    }
  // 一次性任务, 就没有指定period
    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;
    }
// 按固定的频率执行,  这里period是正数
   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)); //  这里period是正数
        RunnableScheduledFuture<Void> t = decorateTask(command, sft);
        sft.outerTask = t;
        delayedExecute(t);
        return t;
    }
 // 按固定的延时执行, 这里period是负数
    public ScheduledFuture<?> scheduleWithFixedDelay(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)); // 这里period是负数
        RunnableScheduledFuture<Void> t = decorateTask(command, sft);
        sft.outerTask = t;
        delayedExecute(t);
        return t;
    }

有两个内部类: 

  1. 将传入的任务Runnable | Callable 封装为 ScheduledFutureTask ,它实现了:
    1. 接口方法java.util.concurrent.Delayed#getDelay的实现是:获取当前时间和下次执行时间的差值 。
    2. 接口方法Comparable<Delayed>#compareTo是比较Delayed#getDelay的值,小的在前面。
  2. 任务存放在延时阻塞队列DelayedWorkQueue里,在有元素变动时堆排序来保证队首元素一定是最近要执行的,判定依据是Comparable<Delayed>#compareTo 。

它本质是一个线程池,从它的构造函数可知:

  1. 只可以指定核心线程数。
  2. DelayedWorkQueue是可变数组(grow 50%:newCapacity = oldCapacity + (oldCapacity >> 1)),容量不超过Integer.MAX_VALUE;

所以当corePoolSize > 0 时, 基本上是不会有超corePoolSize量的线程。

当任务新添入阻塞队列后,会执行ThreadPoolExecutor#ensurePrestart()方法来检测:【当前运行中的线程数量 小于核心线程数或为0】时,增加一个工作线程。

ThreadPoolExecutor#ensurePrestart()

新增任务

新插入到队尾 DelayedWorkQueue#siftUp: 

循环向上,若比三元组内parent还小就与parent交换位置,同时上升到此交换位置作为child 的上一个三元组内比较。(已有的数据已经满足每个三元组结构里parent一定是最小的)

eg.  (n1,n2,n3)、 (n2,n4,n5)、(n3,n6,n7)、 (n4,n8,n9);在每个三元组里parent一定是最小的,即: (n1,n2,n3)里 n1 一定是最小的 ; 同时任意子树root一定是它的全局最小。

任务的执行

DelayedWorkQueue#take() 弹出队首元素

只有当设定了多个核心线程数时,才会有并发。

  1. 首先是经过队列唯一ReentrantLock锁的限制;
  2. 当A工作线程判定队首任务还未到执行时间,A线程挂起等待差额时间,锁被释放(Condition#awaitNanos);“leader” 被绑定为A线程。
  3. B工作线程拿到ReentrantLock权限后, 注意了:  此时判定“leader”是A, B线程会永久挂起(Condition#await())直到被其他线程唤醒! 通过这种方式来防止并发重复问题。当A线程在挂起后其他工作线程也无法弹出任务。
  4. A超时自动苏醒后依旧是第一个处理者。此时会执行DelayedWorkQueue#finishPoll :将队尾元素置顶后,#siftDown 堆排序向下比较,若大于min(left、right ) 就与min交换位置,同时下降到此交换位置作为parent的下一个三元组内比较
// 通过插入时siftUp、弹出siftDown 操作来保证队列里任一子树的根节点一定是最小值, 但不保证全局有序
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); // 已到执行时间, 队列删除队首元素并调整剩下队内的元素的位置 -> siftDown
                 first = null; // don't retain ref while waiting  置空
                 if (leader != null)
                     available.await(); // 已经绑定了工作线程,则当前工作线程挂起
                 else {
                     Thread thisThread = Thread.currentThread();
                     leader = thisThread; 
					 // 绑定唯一的工作线程,主要用来保证在下面awaitNanos时, 依旧可以占着当前队列权限, 防止多线程并发下都自动苏醒后处理到同一个任务
                     try {
                         available.awaitNanos(delay);
                     } finally {
                         if (leader == thisThread)
                             leader = null;
                     }
                 }
             }
         }
     } finally {
         if (leader == null && queue[0] != null)
             available.signal();
         lock.unlock();
     }
 }
/** 弹出原队首任务后, 将队尾任务移动到队首, 再向下比较替换  */
 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);
 }
/** add插入队尾时 队列元素从底向上比较替换 */
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);
  }

ScheduledFutureTask#run

通过“period”来判定是否周期性任务。如果是周期性的,在任务执行完成之后会设置新的NextRunTime,  再将该ScheduledFutureTask添加回阻塞队列中

     public void run() {
            boolean periodic = isPeriodic(); // period != 0 是周期
            if (!canRunInCurrentRunState(periodic))
                cancel(false);
            else if (!periodic)
                ScheduledFutureTask.super.run(); // 非周期性任务执行
            else if (ScheduledFutureTask.super.runAndReset()) {  // 周期性任务FutureTask执行并重置之后
                setNextRunTime(); // 设置下次执行的时间
                reExecutePeriodic(outerTask); // 重新入DelayedWorkQueue队列
            }
        }
    }

ScheduledFutureTask#setNextRunTime方法设置下一次执行时间 :

  1.  period  > 0  :  新“nextExecutionTime” = 原“nextExecutionTime” + “period”。 固定的时间点频率。
  2. period <  0 :    新“nextExecutionTime” =( now())任务执行完成后的System.currentTimeMillis() - “period”。  任务执行完成后按固定延时。
 private void setNextRunTime() {
    long p = period;
    if (p > 0)  time += p;  // 上次的执行时间直接加上period
    else  time = triggerTime(-p); // 当前系统时间加period
 }
long triggerTime(long delay) {
     return now() +  ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}

Timer

内部持有一个线程对象TimerThread和一个存储TimerTask的数组队列TaskQueue(扩容翻倍)

新任务追加到数组的末尾, fixUp 从树底向上比较置换。

在单线程TimerThread#mainLoop()里:

  1. 从队列TaskQueue里拿第一个任务(The tasks are stored in queue[1] up to queue[size]  这么做是为了在堆排序计算(x,2x,2x+1)时更方便)。这里控制并发是用的synchronized与Object#wait(long timeout)
  2. 若是周期性任务,在任务执行开始前就会更新下一次的执行时间并队列内再排序#fixDown。当 period <  0 :    新“nextExecutionTime” =( now())任务执行开始前的System.currentTimeMillis() - “period”。
    private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                synchronized(queue) { // 控制队列并发
                    while (queue.isEmpty() && newTasksMayBeScheduled)
                        queue.wait();
                    if (queue.isEmpty())
                        break; 
                    long currentTime, executionTime;
                    task = queue.getMin(); // 拿队首任务queue[1]
                    synchronized(task.lock) { // 限制一个任务不能被同时处理(eg. 上次还未执行完, 这次又拿到了该任务)
                        if (task.state == TimerTask.CANCELLED) {
                            queue.removeMin();
                            continue;  // No action required, poll queue again
                        }
                        currentTime = System.currentTimeMillis();
                        executionTime = task.nextExecutionTime;
                        if (taskFired = (executionTime<=currentTime)) {
                            if (task.period == 0) { // 一次性任务 直接从队列里移除
                                queue.removeMin();
                                task.state = TimerTask.EXECUTED;
                            } else { // 周期性任务, 重新设置执行时间并塞回队列,将重新排序确定它的位置
                                queue.rescheduleMin(
                                  task.period<0 ? currentTime   - task.period : executionTime + task.period); // fixDown(1)
                            }
                        }
                    }
                    if (!taskFired) // 任务还没到执行时间, 则等待
                        queue.wait(executionTime - currentTime);
                }
                if (taskFired)  // Task fired; run it, holding no locks
                    task.run(); // 执行任务
            } catch(InterruptedException e) { // 忽略InterruptedException,其他的异常会跳出线程
            }
        }
    }

// 队首元素更新了下一次执行时间后, 从底向下比较置换
 private void fixDown(int k) {
        int j;
        while ((j = k << 1) <= size && j > 0) {
            if (j < size &&
                queue[j].nextExecutionTime > queue[j+1].nextExecutionTime)
                j++; // j indexes smallest kid
            if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)
                break;
            TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
            k = j;
        }
    }

// 新加入队列时, 默认加入到队尾, 在fixUp从树底向上比较置换位置。
void add(TimerTask task) {
        // Grow 100%
        if (size + 1 == queue.length)
            queue = Arrays.copyOf(queue, 2*queue.length);

        queue[++size] = task;
        fixUp(size);
 }

  private void fixUp(int k) {
        while (k > 1) {
            int j = k >> 1;
            if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
                break;
            TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
            k = j;
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值