ScheduedThreadPoolExecutor流程及源码详解

理解ScheduedThreadPoolExecutor的原理其实就是对任务的下次执行时间计算以及任务的入队,出队、删除的过程的理解

首先看一下ScheduedThreadPoolExecutor的集成类图

线程池 定时任务 python 定时任务线程池原理_多线程

继承了ThreadPoolExecutor,具有了线程池的功能,实现了ScheduledExecutorService,具有了任务调度的功能,总体来说就是一个定时任务和延迟任务的线程池

线程池 定时任务 python 定时任务线程池原理_线程池_02

ScheduedThreadPoolExecutor和其他的线程池不太一样,它是先把任务放到一个延迟队列中,然后在启动一个线程,再去队列中取周期时间离当前时间最近的那个任务

ScheduedThreadPoolExecutor维护了一个DelayQueue存储等待的任务,DelayQueue里面有一个PriorityQueue优先级队列,他会根据time的时间大小排序,时间越小的越靠前,如果时间相同,就会根据sequenceNumbe排序,这个sequenceNumbe是创建任务的时候初始化的一个数值,DelayQueue也是一个无界队列,但是初始大小为16,超过16会进行一次扩容

ScheduedThreadPoolExecutor有三个方法延迟或者定时任务的方法
调用scheduleAtFixedRate,scheduleWithFixedDelay

  1. Schedule:延迟执行,只会执行一次,有两个不同的参数,入参是Callable的话有返回值,会异步执行,调用ScheduleTaskFuture.get()方法获取返回值
  2. scheduleAtFixedRate:定时任务,周期执行,如果代码逻辑执行时间比自定义的周期时间长,会有任务堆积,这个时间是上次执行的时间+周期的时间
  3. scheduleWithFixedDelay:定时任务,这个时间是当前时间+周期时间,也就是代码逻辑什么时候执行完,执行完后的当前时间+周期时间就是下次任务的执行时间

以上三个方法都会调用delayedExecute(t);

private void delayedExecute(RunnableScheduledFuture<?> task) {
 如果线程池中断就拒绝任务添加
 if (isShutdown())
 reject(task);
 else {
 把任务放到队列中
 super.getQueue().add(task);
 线程池中断并且当前线程池的运行状态不是running状态,移除任务成功,就把这个任务取消
 if (isShutdown() &&
 !canRunInCurrentRunState(task.isPeriodic()) &&
 remove(task))
 task.cancel(false);
 else
 //启动工作线程
 ensurePrestart();
 }
 }

2启动工作线程

void ensurePrestart() {
 获取当前工作线程数
 int wc = workerCountOf(ctl.get());
 if (wc < corePoolSize)
 添加工作线程,这个方法就是ThreadPoolExecutor的方法
 addWorker(null, true);
 else if (wc == 0)
 addWorker(null, false);
 }

3addWork方法中有getTask()方法,这个是从队列总获取任务的,这个任务运行其实就是ScheduledFutureTask
类中的run方法的执行,这个类间接实现了Runnable
接口

public void run() {
 判断是否为周期执行的任务
 boolean periodic = isPeriodic();
 if (!canRunInCurrentRunState(periodic))
 cancel(false);
 else if (!periodic)//如果不是周期执行就调用父类FutureTask的run方法
 ScheduledFutureTask.super.run();
 else if (ScheduledFutureTask.super.runAndReset()) {//周期执行的话调用父类FutureTask的runAndReset方法,这个方法执行完返回ture
 setNextRunTime();//重新设定下次任务的执行时间
 reExecutePeriodic(outerTask);//把任务本身再次放入队列中,再次从队列获取任务
 }
 }


重新计算下次任务的执行时间

private void setNextRunTime() {
 long p = period;
 if (p > 0) scheduleAtFixedRate方法的参数为大于0
 time += p;上次周期结束的时间+周期时间
 else scheduleWithFixedDelay这个方法中自己设置的参数为小于0
 time = triggerTime(-p);当前时间+周期时间
 }

FutureTask的run方法

public void run() {
 if (state != NEW ||
 !UNSAFE.compareAndSwapObject(this, runnerOffset,
 null, Thread.currentThread()))
 return;
 try {
 Callable c = callable;
 if (c != null && state == NEW) {
 V result;
 boolean ran;
 try {
 执行任务
 result = c.call();
 ran = true;
 } catch (Throwable ex) {
 result = null;
 ran = false;
 setException(ex);
 }
 if (ran)

修改状态后结束任务

set(result);
 }
 } finally{
 runner = null;
 int s = state;
 if (s >= INTERRUPTING)
 handlePossibleCancellationInterrupt(s);
 }
 }FutureTask的runAndReset方法
 protected boolean runAndReset() {
 if (state != NEW ||
 !UNSAFE.compareAndSwapObject(this, runnerOffset,
 null, Thread.currentThread()))
 return false;
 boolean ran = false;
 int s = state;
 try {
 Callable c = callable;
 if (c != null && s == NEW) {
 try {
 c.call(); // don’t set result
 ran = true;
 } catch (Throwable ex) {
 setException(ex);
 }
 }
 } finally {
 // runner must be non-null until state is settled to
 // prevent concurrent calls to run()
 runner = null;
 // state must be re-read after nulling runner to prevent
 // leaked interrupts
 s = state;
 if (s >= INTERRUPTING)
 handlePossibleCancellationInterrupt(s);
 }
 执行完直接返回,没有修改线程池状态的操作
 return ran && s == NEW;
 }

现在解释一下delayedWorkQueue
成员变量

private static final int INITIAL_CAPACITY = 16;初始化容量大小
 private RunnableScheduledFuture<?>[] queue =
 new RunnableScheduledFuture<?>[INITIAL_CAPACITY];队列的数据结构是一个数组
 private final ReentrantLock lock = new ReentrantLock();
 private int size = 0;队列大小
 private Thread leader = null; leader线程
 private final Condition available = lock.newCondition();当较新的任务在队列的头部可用时,或者新线程可能需要成为leader,则通过该条件发出信号

1leader线程用于减少不必要的定时等待,是leader-follower的变种
对于多线程的网络模型来说,所有的线程都有这三个状态中的一种: leader,follower,干活中的状态proccessor,他的基本原则就是永远只有一个leader线程,其他follower线程都在等待成为leader,当线程池初始话的时候会自动产生一个leader线程负责等待网络io事件,当有一个事件产生时,leader首先会通知一个follower将其变为leader,自己去干活了,处理完毕之后就变成follower,加入到follower中等待下次变成leader。这种方法可以增强CPU高速缓存相似性,消除动态分配内存和线程间的数据交换

现在介绍一下DelayedWorkQueue的数据结构

DelayedWorkQueue底层的数据结构是一个RunnableScheduledFuture<?>[]数组,但是表现出来的逻辑结构是堆结构(最小堆结构)

线程池 定时任务 python 定时任务线程池原理_多线程_03

下面介绍队列的入队、出队、删除操作
入队操作就是调用的add方法,add方法中调用的offer方法

public boolean offer(Runnable x) {
 if (x == null)
 throw new NullPointerException();
 需要入队的任务这些任务都需要在提交任务的时候用RunnableScheduledFuture这个类封装一下
 RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
 final ReentrantLock lock = this.lock;
 lock.lock();
 try {
 队列的任务个数
 int i = size;
 如果任务个数大于队列的长度,需要扩容
 if (i >= queue.length)
 grow();
 任务个数加1
 size = i + 1;
 第一个任务进来的时候i=0
 if (i == 0) {
 queue[0] = e;
 记录索引
 setIndex(e, 0);
 } else {
 向上比较任务的优先级,周期时间小的放到索引的0位
 siftUp(i, e);
 }

这里有两种情况
1第一个任务进来索引0位就是添加进来的任务,
2调用siftUp这个方法后添加进来的任务被移到索引0位

if (queue[0] == e) {
 Leader设置为null是为了使在take方法中的线程在通过available.signal();后会执行available.awaitNanos(delay);
 leader = null;
 加入元素以后唤醒work线程
 available.signal();
 }
 } finally {
 lock.unlock();
 }
 return true;
 }

任务排序siftUp(i, e);的使用

private void siftUp(int k, RunnableScheduledFuture<?> key) {
 k>0表示不是第一个元素
 while (k > 0) {
 获取索引为k的子节点的父节点,就是(k-1)除以2,这里采用的位运算,效率高,也不会产生小数位
 int parent = (k - 1) >>> 1;
 获取索引为k的父节点的任务
 RunnableScheduledFuture<?> e = queue[parent];

判断key和e的时间大小,如果key的时间大于e的时间,这样这两个就不用交换位置,循环结束,如果小于0,需要交换位置,循环继续,直到把周期时间最小的那个任务置换到最顶层也就是索引为0的位置

if (key.compareTo(e) >= 0)
 break;
 queue[k] = e;
 setIndex(e, k);
 k = parent;
 }
 queue[k] = key;
 setIndex(key, k);
 }

时间大小比较compareTo

public int compareTo(Delayed other) {
 相同的任务返回0
 if (other == this) { // compare zero ONLY if same object
 return 0;
 }
 if (other instanceof DelayedTimer) {
 DelayedTimer x = (DelayedTimer)other;

计算两个任务的周期时间差值

long diff = time - x.time;
 if (diff < 0) {
 return -1;
 } else if (diff > 0) {
 return 1;
 时间相同,比较sequenceNumber的大小
 } else if (sequenceNumber < x.sequenceNumber) {
 return -1;
 } else {
 return 1;
 }
 }
 long d = (getDelay(TimeUnit.NANOSECONDS) -
 other.getDelay(TimeUnit.NANOSECONDS));
 return (d == 0) ? 0 : ((d < 0) ? -1 : 1);
 }

出队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);
 小于等于0说明这个任务到时间去执行了
 if (delay <= 0)
 return finishPoll(first);
 first = null; // don’t retain ref while waiting
 Leader线程不为空说明正在有线程执行任务,因为只有一个leader线程
 if (leader != null)
 available.await();
 else {
 Leader为空,把当前线程置为leader线程
 Thread thisThread = Thread.currentThread();
 leader = thisThread;
 try {
 阻塞到执行时间
 available.awaitNanos(delay);
 } finally {
 Leader置为空,让其他线程执行available.awaitNanos(delay);
 if (leader == thisThread)
 leader = null;
 }
 }
 }
 }
 } finally {
 Leader为空,并且队列有值的话,唤醒一个线程
 if (leader == null && queue[0] != null)
 available.signal();
 lock.unlock();
 }
 }


在讲线程池的时候有讲过runWork中的getTask方法中从队列中取任务take(),但是这是直接把任务取出来就能执行的,现在这个是有时间等待的,只有等待时间到了才能从队列中取出来
这里再次说一下leader的作用
这里的leader的作用是减少不必要的定时等待,当一个线程成为leader后,它只等待下一个时间节点间隔,但其他线程一直等待,leader必须在take或者poll返回之前signal其他线程,这样其他线程才有可能成为leader,除非其他线程成为了leader
假象一下如果没有这个leader,第一个线程取索引为0 的任务后执行available.awaitNanos(delay);这是还没有signal,第二个线程也执行了这段代码,阻塞在这里,多个线程执行该段代码是没有作用
的,因为只能有一个线程会从take中返回queue[0](因为有lock),其他线程这时再返回
for循环执行时取的queue[0],已经不是之前的queue[0]了,然后又要继续阻塞。
所以,为了不让多个线程频繁的做无用的定时等待,这里增加了leader,如果leader不
为空,则说明队列中第一个节点已经在等待出队,这时其它的线程会一直阻塞,减少了无用
的阻塞(注意,在finally中调用了signal()来唤醒一个线程,而不是signalAll())。

Poll方法和take差不多,只不过poll中加入了超时返回null的机制

finishPoll(first)方法
 private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) {
 任务减1
 int s = --size;
 去队列中最后的那个任务
 RunnableScheduledFuture<?> x = queue[s];
 queue[s] = null;
 if (s != 0)
 长度不为0,则从第一个元素开始排序,目的是要把最后一个节点放到合适的位置上
 siftDown(0, x);
 把已经取出的任务索引置为-1,会自动删除
 setIndex(f, -1);
 return f;
 }
 siftDown
 private void siftDown(int k, RunnableScheduledFuture<?> key) {
 根据二叉树的特性,数组长度除以2,表示取有子节点的索引
 int half = size >>> 1;
 判断索引为k的节点是否有子节点
 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© <= 0)
 break;
 如果最大索引的子节点时间间隔大于右子节点
 queue[k] = c;
 setIndex(c, k);
 k = child;
 }
 将key放入索引为k的位置
 queue[k] = key;
 setIndex(key, k);
 }

Siftdown有两种情况

第一种就是没有子节点的

线程池 定时任务 python 定时任务线程池原理_执行时间_04

数组长度是7,除以2就是half=3,当k=3时,不走while,这是索引为最后一个的任务会移到索引为3的位置,就结束了;

线程池 定时任务 python 定时任务线程池原理_线程池 定时任务 python_05

第二种情况k<half

当k=0时,0<3,这时走while,获取左子节点是child=1,右子节点right=2,比较左右子节点的时间间隔大小,

线程池 定时任务 python 定时任务线程池原理_多线程_06

如果right<size,child<right,c=queue[child],queue[k]=c,k=child=1,

线程池 定时任务 python 定时任务线程池原理_线程池 定时任务 python_07

进行下次循环,k=1<3,获取左子节点child=1*2+1=3,right=child+1=4,

线程池 定时任务 python 定时任务线程池原理_多线程_08

这时right<size,child<right,c=queue[3],k=3

线程池 定时任务 python 定时任务线程池原理_多线程_09

最后,如果在finishPoll方法中调用的话,会把索引为0的节点的索引设置为-1,表

示已经删除了该节点,并且size也减了1,最后的结果如下:

线程池 定时任务 python 定时任务线程池原理_执行时间_10

由此可见siftdown并不是有序的,但是可以保证父节点的时间比子节点的小,由于每次都会取左右子节点的最小时间,所以可以保证take或者poll出队是有序的

删除操作remove
 public boolean remove(Object x) {
 final ReentrantLock lock = this.lock;
 lock.lock();
 try {
 获取任务的索引
 int i = indexOf(x);
 小于0返回false
 if (i < 0)
 return false;
 把要删除的任务索引置为-1,代表删除这个任务
 setIndex(queue[i], -1);
 队列中任务数减1
 int s = --size;
 获取队列最后一个数据
 RunnableScheduledFuture<?> replacement = queue[s];
 queue[s] = null;
 索引不一样
 if (s != i) {
 从i开始向下调整
 siftDown(i, replacement);
 如果queue[i] == replacement,说明i是叶子节点
 如果是这种情况,不能保证子节点的下次执行时间比父节点大
 这时需要进行一次向上调整
 if (queue[i] == replacement)
 siftUp(i, replacement);
 }
 return true;
 } finally {
 lock.unlock();
 }
 }

这个是最小堆结构的演示链接
https://www.cs.usfca.edu/~galles/visualization/Heap.html

总结
主要总结为以下几个方面:
与Timer执行定时任务的比较,相比Timer,ScheduedThreadPoolExecutor有什么优点;
ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,所以它也是一个线程池,也有coorPoolSize和workQueue,ScheduledThreadPoolExecutor特
殊的地方在于,自己实现了优先工作队列DelayedWorkQueue;
ScheduedThreadPoolExecutor实现了ScheduledExecutorService,所以就有了任务调度的方法,如schedule,scheduleAtFixedRate和
scheduleWithFixedDelay,同时注意他们之间的区别;内部类ScheduledFutureTask继承自FutureTask,实现了任务的异步执行并且
可以获取返回结果。同时也实现了Delayed接口,可以通过getDelay方法获取将要执行的时间间隔;
周期任务的执行其实是调用了FutureTask类中的runAndReset方法,每次执行完不设置结果和状态。
详细分析了DelayedWorkQueue的数据结构,它是一个基于最小堆结构的优先队列,并且每次出队时能够保证取出的任务是当前队列中下次执行时间最小的任务。
同时注意一下优先队列中堆的顺序,堆中的顺序并不是绝对的,但要保证子节点的值要比父节点的值要大,这样就不会影响出队的顺序。