【Java 线程池】【八】ScheduledThreadPoolExecutor之ScheduledFutureTask、DelayedWorkQueue原理

发布时间 2023-04-13 07:06:25作者: 酷酷-

1  前言

上一节我们看了ScheduledThreadPoolExecutor线程池提交任务的流程。execute、submit、schedule、scheduleAtFixRate方法的原理:都是将任务封装为一个ScheduledFutureTask,然后提交到延迟阻塞队列里面,然后线程池里的线程从延迟阻塞队列中获取到任务,然后执行。那么我们这节就来看看ScheduledFutureTask的原理。

2  ScheduledFutureTask 源码

2.1  ScheduledFutureTask 类结构图

(1)首先继承了FutureTask类,具有FutureTask的功能,至于FutureTask我们之前已经深入分析过了
(2)实现了Delayed接口,具有一个延迟时间属性,存放进入延迟队列的元素必须具有一个延迟时间属性,所以这里实现Delayed接口是必要的

public interface Delayed extends Comparable<Delayed> {
    // Delayed的getDelay接口,返回当前任务或者元素的延迟时间,也就是需要延迟多久
    long getDelay(TimeUnit unit);
}

(3)实现了RunnableScheduledFuture接口,用户判断这个任务是否是周期性任务。

public interface RunnableScheduledFuture<V> extends RunnableFuture<V>, ScheduledFuture<V> {
    // RunnableScheduledFuture的isPeriodic方法返回这个任务是否是周期性任务
    // true,周期性任务
    // false,非周期性任务
    boolean isPeriodic();
}

2.2  核心属性

// 序号id
// 默认情况下延迟任务按照时间优先级执行
// 也就是延迟时间越小,越先执行
// 但是如果两个任务延迟时间一样,则sequenceNumber小的优先级高,先执行
private final long sequenceNumber;

// 延迟时间,任务多久之后会执行
private long time;

// 是否是周期性重复任务
// 是否是0,则不是周期性任务,只是一次性的
// 如果大于0,则每隔period时间会重复执行一次
// 如果小于0,则每隔-period时间重复执行一次
private final long period;

// 这里就是具体的要执行的任务了
RunnableScheduledFuture<V> outerTask = this;

// 在延迟队列中的位置,DelyedWorkQueue延迟队列是基于数组和堆排序实现的
// 而ScheduledFutureTask存储在DelayedWorkQueue延迟队列中,也就是存在数组中
// 这里就是说这个任务是在堆的哪个位置,数组的哪个位置
int heapIndex;

2.3  构造方法

ScheduledFutureTask(Runnable r, V result, long ns) {
    // 调用父类FutureTask的构造方法
    super(r, result);
    // 具体的延迟时间
    this.time = ns;
    // 是否周期性任务,如果为0不是
    this.period = 0;
    // 任务的序号
    // 如果两个任务延时时间一样,需要小的有限执行
    this.sequenceNumber = sequencer.getAndIncrement();
}

ScheduledFutureTask(Runnable r, V result, long ns, long period) {
    super(r, result);
    this.time = ns;
    this.period = period;
    this.sequenceNumber = sequencer.getAndIncrement();
}

2.4  getDelay 方法

实现了Delayed接口的getDelay方法,对外提供一个入口获取任务的延迟时间:

public long getDelay(TimeUnit unit) {
    // 任务的延迟时间就是time - 当前时间,然后转成纳秒单位
    return unit.convert(time - now(), NANOSECONDS);
}

2.5  isPeriodic 方法

实现了RunnableScheduleFuture的isPeriodic接口,判断是否是周期性重复任务:

public boolean isPeriodic() {
    return period != 0;
}

2.6  setNextRunTime 方法

setNextRunTime 如果是周期性重复任务,设置下一次执行的时间:

private void setNextRunTime() {
    long p = period;
    if (p > 0)
        // 下一个周期执行时间为 time+p
        time += p;
    else
        // 下一个周期执行时间为 now() - p
        time = triggerTime(-p);
}

long triggerTime(long delay) {
    return now() +
        ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}

2.7  compareTo 方法

任务的优先级比较,先通过延时时间比较,延迟时间小的优先级高,如果延迟时间一样,sequenceNumber小的优先级高:

public int compareTo(Delayed other) {
    if (other == this) // compare zero if same object
        return 0;
     // 如果Delayed延迟任务是SheduledFutureTask的子类
    if (other instanceof ScheduledFutureTask) {
        ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)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;
    }
    // 如果不是ScheduleFutureTask类的实例,直接比较延迟时间就好了
    long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
    return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
}

2.8  run 方法

运行任务的run方法:

public void run() {
    // 首先获取任务是否是周期性重复任务
    boolean periodic = isPeriodic();
    // 如果当前任务线程池状态不对,取消任务执行
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    // 如果不是周期性的任务,只是一次性延迟任务
    else if (!periodic)
        // 直接调用父类FutureTask的run方法运行任务
        ScheduledFutureTask.super.run();
    // 走到这里periodic为true,说明是周期性重复任务
    else if (ScheduledFutureTask.super.runAndReset()) {
        // 设置下一次运行的时间
        setNextRunTime();
        // 这里就是运行任务,然后将任务重新方法延迟队列中,因为下一次运行的时间修改了
        reExecutePeriodic(outerTask);
    }
}

void reExecutePeriodic(RunnableScheduledFuture<?> task) {
    if (canRunInCurrentRunState(true)) {
        // 再次放入延迟阻塞队列中
        super.getQueue().add(task);
        if (!canRunInCurrentRunState(true) && remove(task))
            task.cancel(false);
        else
            ensurePrestart();
    }
}

看下来重要的可能就是:

(1)ScheduleFutureTask通过实现了Delay接口,具有延迟时间的属性,通过实现了RunnableScheduleFuture接口具有判断是否是周期性重复任务
(2)通过继承了FutureTask具有Future的特性,可以获取任务的结果,对任务进行取消,获取任务的执行状态等。
(3)内部的compareTo为排序的比较方法,首先按照延迟时间进行比较,延迟时间小的优先级高,如果延迟时间一样,那么按照sequenceNumber 比较,越小优先级越高。

3  DelayedWorkQueue 源码

看完ScheduleFutureTask,我们接着继续,看看DelayedWorkQueue是如何实现延迟阻塞队列的。

3.1  接口声明

// DelayedWorkQueue实现了BlockingQueue接口,是一个阻塞队列
static class DelayedWorkQueue extends AbstractQueue<Runnable>
    implements BlockingQueue<Runnable> {
}

3.2  内部属性

// 初始化容量
private static final int INITIAL_CAPACITY = 16;
// 这里就是阻塞队列内部存储数据的结构,是用一个数组来进行存储的
// 数组的初始化长度是16
private RunnableScheduledFuture<?>[] queue =
    new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
// 互斥锁,为实现线程安全和阻塞
private final ReentrantLock lock = new ReentrantLock();
// 等待条件
private final Condition available = lock.newCondition();
// 阻塞队列的大小
private int size = 0;
// leader,第一个等待的线程
private Thread leader = null;

3.3  put、add 方法

put、add底层都是调用offer方法:

public void put(Runnable e) {
    offer(e);
}

public boolean add(Runnable e) {
    return offer(e);
}

public boolean offer(Runnable e, long timeout, TimeUnit unit) {
    return offer(e);
}

3.4  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();
        // 队列大小+1
        size = i + 1;
        // 如果插入前i==0,说明自己是第一个插入元素,直接放入数组的0号元素即可
        if (i == 0) {
            queue[0] = e;
            // 设置当前任务e在数组中的位置
            setIndex(e, 0);
        } else {
            // 如果 i!=0 说明插入队列非空,数组有元素
            // 需要插入任务e,同时进行排序,延迟时间小的排在前面
            // 这里的siftUp方法就是插入和排序
            siftUp(i, e);
        }
        // 如果调整之后,刚插入的元素就是延迟时间最小的元素,唤醒沉睡线程
        if (queue[0] == e) {
            leader = null;
            available.signal();
        }
    } finally {
        // 释放锁
        lock.unlock();
    }
    return true;
}

3.4.1  grow 数组扩容方法

private void grow() {
    // 获取旧数组长度
    int oldCapacity = queue.length;
    // 新数组长度为旧数组长度的1.5倍
    // 比如oldCapacity = 10
    // newCapacity = 10 + (10 > 1) = 10 + 5 = 15
    int newCapacity = oldCapacity + (oldCapacity >> 1); // grow 50%
    // 如果新数组长度小于0,那么新数组长度MAX_VALUE
    if (newCapacity < 0) // overflow
        newCapacity = Integer.MAX_VALUE;
    // 这里就是将旧数组元素拷贝到新数组了,没啥好说的
    queue = Arrays.copyOf(queue, newCapacity);
}

3.4.2  siftUp 插入和排序方法

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);
}

上面的方法,其实就是:
(1)新插入一个元素放入数组尾部
(2)然后通过堆排序,不断调整,形成一个小顶堆

3.5  poll 非阻塞方法

public RunnableScheduledFuture<?> poll() {
    final ReentrantLock lock = this.lock;
    // 获取锁
    lock.lock();
    try {
        // 获取小顶堆的根元素
        RunnableScheduledFuture<?> first = queue[0];
        // 如果为null说明队列时空
        // 如果延迟时间大于当前试下,说明任务还没到执行的时候
        if (first == null || first.getDelay(NANOSECONDS) > 0)
            // 当前没有可以执行的任务,返回null
            return null;
        else
            // 走到这里fisrt非null,并且延迟时间小于当前时间,说明可以执行了
            //
            return finishPoll(first);
    } finally {
        // 释放锁
        lock.unlock();
    }
}

3.5.1  finishPoll 方法

private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) {
    // 队列出队一个元素,大小减少1
    int s = --size;
    RunnableScheduledFuture<?> x = queue[s];
    queue[s] = null;
    // 重新调整堆结构,调整成小顶堆
    if (s != 0)
        siftDown(0, x);
    setIndex(f, -1);
    return f;
}

3.6  take 阻塞方法

public RunnableScheduledFuture<?> take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        // 循环重试
        for (;;) {
            RunnableScheduledFuture<?> first = queue[0];
            // 如果当前队列没有元素
            if (first == null)
                // avaiable 可用条件不满足
                // 调用await方法释放锁,同时进入条件队列进行沉睡等待
                // 等待当队列有元素的时候,调用singal方法唤醒自己
                available.await();
            else {
                
                long delay = first.getDelay(NANOSECONDS);
                // 当前有元素,并且延迟时间小于当前时间,符合执行条件,完成出队
                if (delay <= 0)
                    return finishPoll(first);
                // 走到这里,说明delay > 0 队列中的元素延迟执行时间都大于当前时间
                // 没有可以执行的任务
                first = null; // don't retain ref while waiting
                // 这里leader表示第一个获取元素失败则阻塞的线程
                // 如果leader不为null,前面已经有人等着了,自己只需要等待就可以,别人会唤醒自己
                if (leader != null)
                    // 进入等待队列等待
                    available.await();
                else {
                    // 走到这里说明leader == null
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        // 由于阻塞队列第一个元素至少还有delay时间的延迟
                        // 所以自己只需要等待delay时间,就可以获取到元素
                        // 所以这里就是阻塞等待delay时间
                        available.awaitNanos(delay);
                    } finally {
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        // 当前还有元素,继续唤醒其它等待线程,叫醒别人来取任务了
        if (leader == null && queue[0] != null)
            available.signal();
        // 释放锁
        lock.unlock();
    }
}

DelayedWorkQueue跟之前讲过的延迟队列DelayQueue实现方式非常类似啊,原理基本都是一样的,实现方式也几乎一致。
我们画个图理解一下:

4  小结

好了,到这里ScheduledFutureTask、DelayedWorkQueue我们就看完了,整体的ScheduledThreadPoolExecutor也看的差不多了,有理解不对的地方欢迎指正哈。