一、架构和继承体系关系
1.1 架构图
1.2 核心类:ScheduledThreadPoolExecutor
1.3 内部类:DelayedWorkQueue
- DelayedWorkQueue : 内部是一个 RunnableScheduledFuture接口的数组、存储了按照最小堆数据结构排列的延迟任务。
1.4 ScheduledFutureTask 任务类
记录了任务执行时间、周期(period)、序号;实现了Comparable接口,重写了compareTo()方法,实现按照任务执行时间排序。
period = 0,代表延时任务
period > 0, 表示在任务开始时间+ period
period < 0, 表示在任务执行结束时间+ period
二、基本方法
2.1 延迟执行任务 schedule
//1.延迟执行
schedule(Runnable r, long delay, TimeUnit unit))
//有返回值
schedule(Callable r, long delay, TimeUnit unit))
2.2 scheduleAtFixedRate
scheduleAtFixedRate(Runnable command,long initialDelay,long period,
TimeUnit unit)
- initialDelay 初始延迟多长时间执行。
- period 初始执行时以后每隔多长时间周期执行一次,不论该任务执行多长时间,将下次执行任务扔到队列中。
如图所示:
2.3 scheduleWithFixedDelay
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
周期执行,与上面区别在于,该方法是在任务执行完毕后delay时间再执行,周期时间的开始时间不同
如图所示:
构造函数差异在这里,正负设置
三、核心方法源码解析
类名:DelayedWorkQueue
3.1 take() 获取队列第一个任务
public RunnableScheduledFuture<?> take() throws InterruptedException {
//1.获取sheduled线程池的锁
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
//2.从队列中获取第一个任务
RunnableScheduledFuture<?> first = queue[0];
if (first == null)
//3.如果任务为空,该线程阻塞等待
available.await();
else {
//4.如果不为空,获取还有多长时间执行
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
//已到执行时间,早于或等于;返回任务,见下方法1
return finishPoll(first);
//5.还没到任务执行时间,等待时将该引用置空,
first = null; // don't retain ref while waiting
//如果有leader线程在执行,当前线程阻塞等待
if (leader != null)
available.await();
else {
//将当前线程设置为leader线程
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
//线程阻塞等待delay时间
available.awaitNanos(delay);
} finally {
//阻塞等待唤醒,将leader线程置空,继续for循环
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && queue[0] != null)
//当前线程已经获取到第一个任务,队列中还有其他任务待执行,唤醒一个其他阻塞线程
available.signal();
//释放锁
lock.unlock();
}
}
方法1,取出第一个元素,并对堆重排序
//该方法在获取锁的情况下执行,不存在多线程问题
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;
}
/**
* 如果是周期任务,给任务堆索引赋值
* Sets f's heapIndex if it is a ScheduledFutureTask.
*/
private void setIndex(RunnableScheduledFuture<?> f, int idx) {
if (f instanceof ScheduledFutureTask)
((ScheduledFutureTask)f).heapIndex = idx;
}
3.2 删除节点 下溯
方法2,最小堆自上而下重排序
堆前备概念知识:
- 1.根节点始终在数组索引为0位置
- 2.假设任意非根节点索引为i, 则其父节点索引为 (i-1)/2
- 3.假设节点索引为i, 它的左子节点若存在,索引为 2i+1,右子节点若存在,索引为 2i+2
/**
* Sifts element added at top down to its heap-ordered spot.
* Call only when holding lock.
* k 索引位置,一般为根节点0,key, 最后一个任务
*/
private void siftDown(int k, RunnableScheduledFuture<?> key) {
// 等同于 size/2,获取数组中间位置索引,即左边第一个叶子节点
// 根据上面公式3, half *2 +1 >= size,索引在大于等于size下标的元素为空,
// 因此不存在子节点
int half = size >>> 1;
//循环向下的截止条件是最后一个非叶子节点
while (k < half) {
//获取左子节点
/**
* 1
* 2 3
*/
int child = (k << 1) + 1;
RunnableScheduledFuture<?> c = queue[child];
//右子节点索引
int right = child + 1;
if (right < size && c.compareTo(queue[right]) > 0)
//若右子节点存在,且小于左子节点,c赋值为右子节点,且child指向右子节点索引
c = queue[child = right];
//如果待排序节点都小于等于,原左右子节点,跳出循环
if (key.compareTo(c) <= 0)
break;
//1.将其左或右子节点,即较小的那个,向上移动;若相等,移动左子节点
queue[k] = c;
setIndex(c, k);
//child此时是较小子节点位置,继续循环向下遍历
k = child;
}
//k为将要插入的位置,将最后一个元素放在该位置,此时堆满足最小堆结构
queue[k] = key;
//同时给该节点设置堆索引
setIndex(key, k);
}
3.3 offer(Runnable task) 添加任务
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)
//扩容,每次1.5倍增加
grow();
//个数+1
size = i + 1;
if (i == 0) {
//若原来队列中任务为空,直接放入队列
queue[0] = e;
setIndex(e, 0);
} else {
//加入队列,更新堆顺序
siftUp(i, e);
}
if (queue[0] == e) {
//如果当前任务是最先执行的,将leader线程置空,唤醒一个线程处理
leader = null;
available.signal();
}
} finally {
lock.unlock();
}
return true;
}
3.4 添加节点上溯siftUp()
自下向上堆排序
/**
* Sifts element added at bottom up to its heap-ordered spot.
* Call only when holding lock.
* 插入是从队尾开始
* k=size
*/
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);
}
3.5 ScheduledFutureTask类compareTo()方法
通过重写该方法,实现最小堆排序
先比较任务执行时间,先执行的在前面,若相同则序号小的排在前,线程池维护了一个原子自增器,每个加入的任务,获取一个自增序号。
public int compareTo(Delayed other) {
if (other == this) // compare zero if same object
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;
}
3.6 ScheduledFutureTask 的run()方法
public void run() {
//判断是否为周期任务
boolean periodic = isPeriodic();
if (!canRunInCurrentRunState(periodic))
cancel(false);
else if (!periodic)
//立即执行
ScheduledFutureTask.super.run();
else if (ScheduledFutureTask.super.runAndReset()) {
//周期任务,执行完毕,设置下一次执行时间
setNextRunTime();
reExecutePeriodic(outerTask);
}
}
/**
* Sets the next time to run for a periodic task.
* 设置下一次执行时间
*/
private void setNextRunTime() {
long p = period;
if (p > 0)
// 大于0,基于上一次任务开始时间 + p
time += p;
else
// p小于则当前时间+ -p
time = triggerTime(-p);
}
long triggerTime(long delay) {
return now() +
((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}
四、与Timer对比
Timer :
- 1.timer是单线程的,如果定时任务多,某个任务执行时间长,会影响后面的任务执行;
- 2.其中某个任务发送异常,未捕获,会导致线程退出,所有任务无法继续执行
ScheduledTheadPoolExecutor : 多线程的,线程退出,线程池会重新创建新的线程执行
五、总结
最小堆数据结构
Leader-follower模型:资源有效利用,既实现多线程并发处理任务,又保证不是每个任务都无效忙等待
六、应用场景
1.Nacos等注册中心应该心跳检测,定时向server发送心跳,检测服务是否可用
2.分布式锁续期、redisson框架WatchDog 每隔1/3 过期时间向redis续期等应用
3.服务启动定时上报服务器运行状态,cpu,内存等指标