类结构图

它用来处理延时任务或定时任务。

它接收SchduledFutureTask类型的任务,是线程池调度任务的最小单位,有三种提交任务 的方式:
- schedule
- scheduledAtFixedRate
- scheduledWithFixedDelay
它采用DelayQueue存储等待的任务
- DelayQueue内部封装了一个PriorityQueue,它会根据time的先后时间排序,若 time相同则根据sequenceNumber排序;
- DelayQueue也是一个无界队列;
SchduledFutureTask SchduledFutureTask接收的参数(成员变量):
- private long time:任务开始的时间
- private final long sequenceNumber;:任务的序号
- private final long period:任务执行的时间间隔
工作线程的执行过程: 工作线程会从DelayQueue取已经到期的任务去执行; 执行结束后重新设置任务的到期时间,再次放回DelayQueue
ScheduledThreadPoolExecutor会把待执行的任务放到工作队列DelayQueue中, DelayQueue封装了一个PriorityQueue,PriorityQueue会对队列中的元素进行排列。
ScheduledThreadPoolExecutor
先来看看ScheduledThreadPoolExecutor的测试。
public class ScheduledThreadPoolExecutorTest {
public static void main(String[] args) {
ScheduledThreadPoolExecutor scheduled = new ScheduledThreadPoolExecutor(2);
scheduled.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("time " + new Date());
}
}, 0, 3, TimeUnit.SECONDS);
}
}
time Wed Apr 02 16:01:04 CST 2025
time Wed Apr 02 16:01:07 CST 2025
time Wed Apr 02 16:01:10 CST 2025
先进入ScheduledThreadPoolExecutor的构造函数。
public ScheduledThreadPoolExecutor(int corePoolSize) {
// corePoolSize核心线程数
// 最大线程数 Integer.MAX_VALUE
// 如果超出corePoolSize数的Worker,只要从队列中拉取不到数据,立即回收
// DelayedWorkQueue 延迟队列
super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS,
new DelayedWorkQueue());
}
上面的构造函数实际上是调用了ThreadPoolExecutor的构造函数来初始化的。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
和ThreadPoolExecutor使用唯一区别就是使用的阻塞队列不同 。DelayedWorkQueue这个队列其实和DelayQueue的实现原理一模一样
scheduleAtFixedRate
我们进入scheduleAtFixedRate方法。
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit) {
// 如果任务为空或 单位为空,则抛出NullPointerException异常
if (command == null || unit == null)
throw new NullPointerException();
// 如果期限小于等于 0 ,则抛出 IllegalArgumentException
if (period <= 0)
throw new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(period));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
protected <V> RunnableScheduledFuture<V> decorateTask(
Runnable runnable, RunnableScheduledFuture<V> task) {
return task;
}
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
ScheduledFutureTask(Runnable r, V result, long ns, long period) {
super(r, result);
// 任务加入线程池后多长时间开始运行
this.time = ns;
// 第一次任务执行后,每隔多长时间再执行一次任务 , 为什么这么说呢 ?
// 假如time = 5 秒,period = 3 秒,当前时间为 2022-04-19 11:18:00
// 1. 第一次任务执行时间 2022-04-19 11:18:05
// 2. 第二次任务执行时间 2022-04-19 11:18:08
// 3. 第三次任务执行时间 2022-04-19 11:18:11
// 3. 第四次任务执行时间 2022-04-19 11:18:14
// ... 依此类推
this.period = period;
// 当前任务的序列号,用于compareTo比较,当时间一样的情况下,比较sequenceNumber
this.sequenceNumber = sequencer.getAndIncrement();
}
private long triggerTime(long delay, TimeUnit unit) {
return triggerTime(unit.toNanos((delay < 0) ? 0 : delay));
}
long triggerTime(long delay) {
return now() +
((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}
private long overflowFree(long delay) {
Delayed head = (Delayed) super.getQueue().peek();
if (head != null) {
long headDelay = head.getDelay(TimeUnit.NANOSECONDS);
if (headDelay < 0 && (delay - headDelay < 0))
delay = Long.MAX_VALUE + headDelay;
}
return delay;
}
我相信triggerTime()这个方法理解起来,大家有点晕,我们来看一个例子。
private long triggerTime(long delay, TimeUnit unit) {
return triggerTime(unit.toNanos((delay < 0) ? 0 : delay));
}
long triggerTime(long delay) {
return now() +
((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}
private long overflowFree(long delay) {
Delayed head = (Delayed) super.getQueue().peek();
if (head != null) {
long headDelay = head.getDelay(TimeUnit.NANOSECONDS);
if (headDelay < 0 && (delay - headDelay < 0))
delay = Long.MAX_VALUE + headDelay;
}
return delay;
}
如果用Long.MAX_VALUE来举例的话,可能大家被吓晕了,那我们用byte来举例, byte的最大值为127,
最小值为-128 ,如果此时delay=72,但延迟队列中时间最小的为-57,在延迟队列用compareTo()方法比较过程中,
会用-57 - 72 = -129 导致越界,因此作者采用的办法是将delay值改掉,delay = 127 + 57 = 70,再次用
compareTo()方法比较时,70 - 57 = -127 ,不会导致byte越界了,将byte换成Long类型,原理一样,
只是byte理解起来方便 。
上面提到过compareTo()方法,从之前的博客中知道,ScheduledThreadPoolExecutor用到的延迟工作队列实际上是用一棵小顶堆树来实现,因此需要比较两个任务谁的执行时间小。
public int compareTo(Delayed other) {
if (other == this)
return 0;
if (other instanceof ScheduledFutureTask) {
ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
// time 表示某个具体时间的纳秒值,如2022-04-19 00:00:00 的纳秒值
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);
}
下面来看,将当前任务添加到延迟队列中。
delayedExecute()
private void delayedExecute(RunnableScheduledFuture<?> task) {
// 如果当前线程池状态是SHUTDOWN,STOP,TIDYING,TERMINATED,则通过拒绝策略来处理任务
if (isShutdown())
reject(task);
else {
// 将当前任务添加到延迟工作队列中
super.getQueue().add(task);
// 如果此时当前线程池状态是SHUTDOWN,STOP,TIDYING,TERMINATED
if (isShutdown() &&
// 如果当前线程池state是不可运行状态 ,则将新加入的task从队列中remove掉
// 如果是 STOP,TIDYING,TERMINATED ,则肯定会将task从队列中移除
// 如果是SHUTDOWN 并且 period> 0 ,并且 continueExistingPeriodicTasksAfterShutdown == true
// 则将 task 从队列中移除
// 如果state是SHUTDOWN并且 period == 0 并且 executeExistingDelayedTasksAfterShutdown = true ,
// 则将task从队列中移除
!canRunInCurrentRunState(task.isPeriodic()) &&
// 移除已经加入到队列中的task
remove(task))
// 将当前任务的状态设置为 CANCELLED
task.cancel(false);
else
ensurePrestart();
}
}
boolean canRunInCurrentRunState(boolean periodic) {
// 下面分两种情况
// 1. 如果period == 0,表示定时任务只执行一次。
// a) executeExistingDelayedTasksAfterShutdown == true ,当然默认为true ,当前线程池状态为RUNNING或SHUTDOWN,则返回true
// b) executeExistingDelayedTasksAfterShutdown == false, 可以手动设置 ,当前线程池状态为RUNNING则返回true
// 2. 如果period > 0
// a) continueExistingPeriodicTasksAfterShutdown ==true,默认为false,当前线程池状态为RUNNING或SHUTDOWN则返回true
// b) continueExistingPeriodicTasksAfterShutdown == false,当前线程池状态为RUNNING,则返回true
return isRunningOrShutdown(periodic ?
continueExistingPeriodicTasksAfterShutdown :
executeExistingDelayedTasksAfterShutdown);
}
final boolean isRunningOrShutdown(boolean shutdownOK) {
int rs = runStateOf(ctl.get());
return rs == RUNNING || (rs == SHUTDOWN && shutdownOK);
}
// 至少确保有一个Worker在工作
void ensurePrestart() {
// 计算当前线程池中的Worker数
int wc = workerCountOf(ctl.get());
if (wc < corePoolSize)
addWorker(null, true);
// 如果wc == 0 ,则允许添加最大的Worker数为Integer.MAX_VALUE
else if (wc == 0)
addWorker(null, false);
}
// 上面这段代码有区分,正常情况下,如果 线程池中Worker数不会超过corePoolSize数,
// 但有特殊情况,我们知道maximumPoolSize = Integer.MAX_VALUE,
// 如果corePoolSize = 10 ,当前线程池中Worker数为0,突然向线程池中添加了100个任务
// 100个任务同时看到了 当前线程池中的Worker数为0,则会调用addWorker(null, false);
// 方法去添加Worker,但addWorker(null, false); 第二个参数为false,意味着
// 只要当前Worker数小于maximumPoolSize数,都可以将Worker添加到线程池中,在这种极端的情况下
// 可能线程池中的Worker数大于 corePoolSize 数。 作者这么做的原因可能也是想让尽可能多的任务分配到Worker来执行
// 从而减少执行时间的延迟
run()
我们提交的任务被封装到ScheduleFutureTask中,线程池中线程拿到任务时回去执行该run()方法
接下来,我们来看其run()方法
public void run() {
boolean periodic = isPeriodic();
// 如果当前是不可运行状态,则唤醒在等待get()返回值的线程,同时将任务的状态设置为CANCELLED
if (!canRunInCurrentRunState(periodic))
cancel(false);
else if (!periodic)
// 如果period == 0 ,表示不需要同期性执行的任务,则直接调用FutureTask的run()方法,最终调用了call()方法
ScheduledFutureTask.super.run();
// 如果需要周期性执行的任务,并且重置任务的state状态,设置下一次执行时间
else if (ScheduledFutureTask.super.runAndReset()) {
// 重置下一次运行时间
setNextRunTime();
// 当前任务重新入队
reExecutePeriodic(outerTask);
}
}
1. 如果当前线程池运行状态不可以执行的任务,取消该任务,然后直接返回,否则继续步骤2
2. 如果不是周期性任务,调用FutureTask中的run方法执行,会设置执行结果,然后直接返回,否则执行步骤3
3. 如果是周期性任务,调用FutureTask中的runAndReset方法执行,不会设置执行结果,然后直接返回,否则执行步骤4和步骤5
4. 计算下次执行该任务的具体时间 。
5. 重复执行该任务
protected boolean runAndReset() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return false;
boolean ran = false;
int s = state;
try {
Callable<V> c = callable;
if (c != null && s == NEW) {
try {
c.call(); // don't set result
ran = true;
} catch (Throwable ex) {
setException(ex);
}
}
} finally {
runner = null;
s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
return ran && s == NEW;
}
private void setNextRunTime() {
long p = period;
if (p > 0)
time += p;
else
time = triggerTime(-p);
}
接下来看当前任务重新入队的逻辑
void reExecutePeriodic(RunnableScheduledFuture<?> task) {
// 当前线程池的状态是RUNNING
if (canRunInCurrentRunState(true)) {
// 将当前任务加入到队列中
super.getQueue().add(task);
// 如果此时线程池状态变为不可运行状态,则将任务从队列中移除
if (!canRunInCurrentRunState(true) && remove(task))
task.cancel(false);
else
// 看是否需要加Worker来处理任务
ensurePrestart();
}
}
该方法和delayedExecute方法类似,不同的是:
- 由于调用reExecutePeriodic方法时已经执行过一次周期性任务了,所以不会 reject当前任务;
- 传入的任务一定是周期性任务。
总结
DelayedWorkQueue
ScheduledThreadPoolExecutor之所以要自己实现阻塞的工作队列,是因为 ScheduledThreadPoolExecutor要求的工作队列有些特殊。
DelayedWorkQueue是一个基于堆的数据结构,类似于DelayQueue和 PriorityQueue。在执行定时任务的时候,每个任务的执行时间都不同,所以 DelayedWorkQueue的工作就是按照执行时间的升序来排列,执行时间距离当前时间越近 的任务在队列的前面(注意:这里的顺序并不是绝对的,堆中的排序只保证了子节点的下次 执行时间要比父节点的下次执行时间要大,而叶子节点之间并不一定是顺序的
ScheduedThreadPoolExecutor有 什么优点;
- ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,所以它也是一 个线程池,也有coorPoolSize和workQueue,ScheduledThreadPoolExecutor特 殊的地方在于,自己实现了优先工作队列DelayedWorkQueue;
- ScheduedThreadPoolExecutor实现了ScheduledExecutorService,所以就有 了任务调度的方法,如schedule,scheduleAtFixedRate和 scheduleWithFixedDelay,同时注意他们之间的区别;
- 内部类ScheduledFutureTask继承自FutureTask,实现了任务的异步执行并且 可以获取返回结果。同时也实现了Delayed接口,可以通过getDelay方法获取将要执 行的时间间隔;
- 周期任务的执行其实是调用了FutureTask类中的runAndReset方法,每次执行 完不设置结果和状态。
- 详细分析了DelayedWorkQueue的数据结构,它是一个基于最小堆结构的优先 队列,并且每次出队时能够保证取出的任务是当前队列中下次执行时间最小的任务。 同时注意一下优先队列中堆的顺序,堆中的顺序并不是绝对的,但要保证子节点的值 要比父节点的值要大,这样就不会影响出队的顺序。
- 总体来说,ScheduedThreadPoolExecutor的重点是要理解下次执行时间的计算,以及优 先队列的出队、入队和删除的过程,这两个是理解ScheduedThreadPoolExecutor的关 键。

2065

被折叠的 条评论
为什么被折叠?



