java.util.Timer
java.util.concurrent.ScheduledThreadPoolExecutor
简称STPE定时任务基本上都是在一个while(true)
或for(;;)
死循环中(每次循环判断定时程序是否终止或暂停),从任务存放的地(可以是内存的堆结构,可以是远程数据库获取,可以是阻塞队列)获取最近要执行的任务,获取的最近的任务没到时间就阻塞一段时间,阻塞可以用Object.wait
或Condition::awaitNanos
。对于周期执行的任务,每次执行完毕将下一个周期的自身再次加入任务存放的地方。
除此之外,一个完善的定时框架要考虑执行线程池、如何处理过期任务(顺序执行?丢弃?覆盖?)、重试策略、监听器等方面问题。分布式定时任务还涉及到rpc、任务的分片、执行器的负载均衡执行策略等问题。
public static void main(String[] args) {
// daemon
Timer timer = new Timer(true);
// 每秒执行一次
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("timertask running at "+ new Date());
}
},0,1000);
Thread.sleep(10000);
}
Corresponding to each Timer object is a single background thread that is used to execute all of the timer’s tasks, sequentially. If a timer task takes excessive time to complete, it “hogs” the timer’s task execution thread. This can, in turn, delay the execution of subsequent tasks, which may “bunch up” and execute in rapid succession when (and if) the offending task finally completes.
This class is thread-safe: multiple threads can share a single Timer object without the need for external synchronization.
This class does not offer real-time guarantees: it schedules tasks using the Object.wait(long) method.
Java 5.0 introduced ScheduledThreadPoolExecutor which is a versatile replacement for the Timer/TimerTask combination, as it allows multiple service threads, accepts various time units, and doesn’t require subclassing TimerTask (just implement Runnable).
Internally, it uses a binary heap to represent its task queue, so the cost to schedule a task is O(log n), where n is the number of concurrently scheduled tasks.
可以看出,一个Timer中只有一个线程里执行调度任务,所以一个任务执行时间过长会影响后续任务,使用wait方法调度,任务放在一个二叉堆中。不推荐使用。
Timer中包含一个执行线程和一个任务队列:
// 根据nextExecutionTime排序的堆
private final TaskQueue queue = new TaskQueue();
// 传入queue执行
private final TimerThread thread = new TimerThread(queue);
定时任务使用TimerTask代替,TimerTask包含4种状态以及一个period来代表执行策略:
static final int VIRGIN = 0;
static final int SCHEDULED = 1;
static final int EXECUTED = 2;
static final int CANCELLED = 3;
// positive value : fixed-rate execution
// negative value : fixed-delay execution
// 0 : non-repeating task
long period = 0;
来看看TimerThread
如何安排 TaskQueue
中的TimerTask
:
class TimerThread extends Thread {
// 代表是否还有任务,没有会优雅终止调度
boolean newTasksMayBeScheduled = true;
private TaskQueue queue;
public void run() {
try {
mainLoop();
} finally {
// Someone killed this Thread, behave as if Timer cancelled
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// 没有任务且newTasksMayBeScheduled为true就挂起
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die
// Queue nonempty; look at first evt and do the right thing
long currentTime, executionTime;
// 最近的任务
task = queue.getMin();
synchronized(task.lock) {
// 是否取消
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) { // Non-repeating, remove
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule
// 定时重复执行则调整到该任务下一次执行时间,queue会重新堆化
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
// 没超时就wait一些时间
if (!taskFired) // Task hasn't yet fired; wait
queue.wait(executionTime - currentTime);
}
// 判断执行
if (taskFired) // Task fired; run it, holding no locks
task.run();
} catch(InterruptedException e) {
}
}
}
}
可以看到Timer是获取最近时间的定时任务,如果没到时间线程会挂起executionTime - currentTime时间。
public static void main(String[] args) {
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(3);
executor.scheduleAtFixedRate(() -> System.out.println("running at "+ new Date()),0,1, TimeUnit.SECONDS);
}
This class is preferable to Timer when multiple worker threads are needed, or when the additional flexibility or capabilities of ThreadPoolExecutor (which this class extends) are required.
Tasks scheduled for exactly the same execution time are enabled in first-in-first-out (FIFO) order of submission. When a submitted task is cancelled before it is run, execution is suppressed. By default, such a cancelled task is not automatically removed from the work queue until its delay elapses. While this enables further inspection and monitoring, it may also cause unbounded retention of cancelled tasks. To avoid this, setsetRemoveOnCancelPolicy
to true, which causes tasks to be immediately removed from the work queue at time of cancellation.
It acts as a fixed-sized pool using corePoolSize threads and an unbounded queue, adjustments to maximumPoolSize have no useful effect.
可以看出,定时任务线程池设置固定和核心线程数和无界任务队列,最大线程数设置无意义,默认为Integer.MAX_VALUE
。另外,提交一个cancelled的任务不会执行,且默认不移除任务队列,当时间过了才移除,可能会导致cancelled任务堆积,可设置setRemoveOnCancelPolicy
为true。
任务使用private class ScheduledFutureTask
代表
套路也是一样,创建一个任务加入执行schedule或scheduleAtFixedRate方法时,会创建一个ScheduledFutureTask
加入任务队列,然后STPE
的父类TPE
中会在一个无限循环中从任务队列DelayedWorkQueue
中拉取任务执行,关于该任务队列:
A DelayedWorkQueue is based on a heap-based data structure like those in DelayQueue and PriorityQueue, except that every ScheduledFutureTask also records its index into the heap array.
This eliminates the need to find a task upon cancellation, greatly speeding up removal (down from O(n) to O(log n)), and reducing garbage retention that would otherwise occur by waiting for the element to rise to top before clearing.
与Timer
不同的是,该阻塞队列阻塞用的是JUC的Condition::await
那一套,而不是Object::wait
;相同的是如果获取到的任务还没到达时间,同样也是需要挂起一段时间,挂起用的是Conditioni::awaitNanos
,参看如下代码:
static class DelayedWorkQueue extends AbstractQueue<Runnable>
implements BlockingQueue<Runnable> {
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);
first = null; // don't retain ref while waiting
// Thread designated to wait for the task at the head of the queue. This variant of the Leader-Follower pattern serves to minimize unnecessary timed waiting.
// leader不为空则挂起
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
// leader为空则该线程设为leader
leader = thisThread;
try {
// **********
// 挂起相应时间
// **********
available.awaitNanos(delay);
} finally {
// 清空
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
// 唤醒follower
if (leader == null && queue[0] != null)
available.signal();
lock.unlock();
}
}
}
这里有一个 Leader-Follower Pattern,可参考Explain “Leader/Follower” Pattern
执行完了会将下一个周期要执行的新任务加入任务队列,参见ScheduledFutureTask::run和::reExecutePeriodic
:
private class ScheduledFutureTask<V>
extends FutureTask<V> implements RunnableScheduledFuture<V> {
...
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);
}
}
...
}
可以看到STPE
和Timer
策略一样,获取最近时间的定时任务,如果没到时间线程会挂起executionTime - currentTime时间,不过这个步骤在Timer
中是由执行线程TimerThread
完成,而不是存储任务的TaskQueue
,而在STPE
中,该步骤放在了阻塞队列DelayedWorkQueue
中完成。
// Job
static class TestJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println("TestJob executing~");
}
}
public static void main(String[] args) {
// Scheduler
Scheduler defaultScheduler = StdSchedulerFactory.getDefaultScheduler();
defaultScheduler.start();
// JobDetail
JobDetail jobDetail = JobBuilder.newJob(TestJob.class).withIdentity("testJob", "default").build();
// Trigger
TriggerBuilder<CronTrigger> triggerBuilder = TriggerBuilder.newTrigger()
.withIdentity("trigger", "default")
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule("0 * * * * ?"));
CronTrigger cronTrigger = triggerBuilder.build();
defaultScheduler.scheduleJob(jobDetail,cronTrigger);
}
TimerThread
主要的流程都集中在QuartzSchedulerThread::run
中,源码略。
可以看到QuartzSchedulerThread
从QuartzSchedulerResources.getJobStore().acquireNextTriggers(...)
获取一段时间内可以执行的任务,获取失败第一次会通知监听器,后面会有指数退避,但不会通知监听器。后面先判断是否到了运行时间,如果离运行时间在2ms内则直接运行,否则调用Object::wait
方法挂起waitTime - now。如果不用挂起,则初始化运行结果TriggerFiredResult
,结果中获取TriggerFiredBundle
,将TriggerFiredBundle
转化为JobRunShell
,最后实际运行QuartzSchedulerResources.getThreadPool().runInThread(shell)
。
QuartzSchedulerResources.getJobStore().acquireNextTriggers(...)
中获取任务,其中也会做任务是否过期的判断,如RAMJobStore中:
public List<OperableTrigger> acquireNextTriggers(long noLaterThan, int maxCount, long timeWindow) {
...
// 判断任务是否过期
if (applyMisfire(tw)) {
if (tw.trigger.getNextFireTime() != null) {
timeTriggers.add(tw);
}
continue;
}
protected boolean applyMisfire(TriggerWrapper tw) {
long misfireTime = System.currentTimeMillis();
if (getMisfireThreshold() > 0) {
misfireTime -= getMisfireThreshold();
}
Date tnft = tw.trigger.getNextFireTime();
// tw.trigger.getMisfireInstruction() Trigger接口中有两种
// MISFIRE_INSTRUCTION_SMART_POLICY 根据updateAfterMisfire()判读但
// MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY 忽略
// CronTrigger还有两种:
// MISFIRE_INSTRUCTION_FIRE_ONCE_NOW
// MISFIRE_INSTRUCTION_DO_NOTHING
// SimpleTrigger还有两种:
// MISFIRE_INSTRUCTION_FIRE_NOW
// MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT
if (tnft == null || tnft.getTime() > misfireTime
|| tw.trigger.getMisfireInstruction() == Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY) {
return false;
}
...
}
}
Quartz没有可执行的任务会使用Object::wait
方法挂起,整个加锁和挂起等待用的是synchronized和Object::wait
那一套,和Timer
类似。不同的是,Quartz每次获取的是一段时间内可执行的一批任务。
XXL-JOB 分布式任务调度平台
分布式调用平台涉及分布式环境的交互,所以rpc是不可避免地,XXL-JOB中使用netty框架构建了基于HTTP的rpc交互方式(XXL-RPC)
其中,调度中心是一个前后端不分离、完整的Spring Web项目,完成任务调度,xxl-job-admin同时依赖xxl-job-core。而每个执行器(任务执行的客户端,即业务集群节点)也需要添加依赖xxl-job-core,在执行器端的该依赖中,XXL基于Netty构建了自己的http接收方式来接收来自调度中心的各种请求,如:任务执行、终止、日志请求,参见EmbedServer
和EmbedHttpServerHandler
;执行器客户端在启动时也要向调度中心注册该执行器(客户端集群节点信息,非任务信息,BEAN模式下任务维护在执行器端的ConcurrentHashMap
中,不向调度中心注册),同时调度中心会通过Java原生http向各个执行器发送心跳来检测是否存活以及检测任务执行状态,执行器执行将结果放入一个结果阻塞队列,线程从队列中获取结果向调度中心发送消息,该发送过程也是通过Java原生http完成的,调度中心和执行发送http请求共用XxlJobRemotingUtil
工具类来完成。
从以上的描述可以看出:
调度中心 | 执行器 | |
---|---|---|
http发送方式 | java原生http | java原生http |
http接收方式 | Spring Web | Netty |
调度中心用于和执行器交互的api(Spring Web构建)只有3个,可视化平台还有其他的不做讨论:
执行器的api(netty构建)有5个:
注意,心跳检测是执行器向调度中心每隔30s向*/api/registry*发送心跳(参见ExecutorRegistryThread
),该api同样也是注册接口;而执行器端的 /beat是调度中心在Failover路由模式下向执行器分配任务时主动检测时用的,只能说作者api命名地太抽象了。
调度中心调度同样和上面地几个调度方式差不多,XXL-JOB在一个while(!stop)
循环中,向数据库查询一段时间内(默认接下来的5000ms)地任务, 然后判断任务是否过期,如果过期超过5000ms要看你的过期策略是什么;如果过期没超过5000ms就立即执行,且看看你下次执行是否又在下一个5000ms内,在的话加入时间轮;时间没到也加入时间轮。时间轮中存放key
和对应的List
,其中key=((jobInfo.getTriggerNextTime()/1000)%60)
,然后另外有一个线程每隔一秒拿出里面的List
遍历获取任务分配给执行器执行,分配路由策略有多种:
执行完了要更新任务下一轮触发时间更新数据库,包括任务执行结果、日志、下一轮任务trigger时间等。以上代码主要集中在JobScheduleHelper
类中。
可以看到在任务时间没到之前,XXL-JOB不像之前的定时任务那样采用线程挂起的方式,而是使用时间轮存储任务id,另起一个线程每隔一秒从时间轮获取当前时间的任务id来分配给执行器执行。