一、理论基础(底层算法)
1、小顶堆
小顶堆是一颗完全二叉树(除了最后一层外其它层都达到最大节点数,且最后一层节点都是靠左排列);
堆中某个节点的值总是不大于其父节点的值,也就是说根节点的值是最小的。
堆化:
插入元素:插入尾部,然后上浮。
删除对顶元素:将尾部(最大的)元素放到堆顶,然后下沉。
2、时间轮算法
(1)链表或者数组实现时间轮:遍历数组,每个下标放置一个链表,链表节点放置任务,遍历到了就取出执行。
(2)round型时间轮:任务上记录一个round,遍历到了就将round减1,为0时取出执行,但由于需要遍历所有任务,效率较低。
(3)分层时间轮:使用多个不同时间纬度的轮。例如月轮遍历到了,将任务取出放到天轮里面,即可实现几号几点执行。
二、JDK定时器timer使用
package com.tuling.timer;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class TimerTest {
public static void main(String[] args) {
Timer t = new Timer();//任务启动
for (int i=0; i<2; i++){
TimerTask task = new FooTimerTask("foo"+i);
t.scheduleAtFixedRate(task,new Date(),2000);//任务添加 10s 5次 4 3
// 预设的执行时间nextExecutTime 12:00:00 12:00:02 12:00:04
//schedule 任务执行超时,会导致后面的任务往后推移,预想在这个间隔内存的任务执行就没了
//scheduleAtFixedRate 任务超时可能导致下一个任务就会马上执行
//单线程 任务阻塞 任务超时
}
}
}
class FooTimerTask extends TimerTask {
private String name;
public FooTimerTask(String name) {
this.name = name;
}
public void run() {
try {
System.out.println("name="+name+",startTime="+new Date());
Thread.sleep(3000);
System.out.println("name="+name+",endTime="+new Date());
//线程池执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
三、定时任务线程池
使用多线程执行任务,不会相互阻塞;如果线程失活,会新建线程执行任务。
package com.tuling.pool;
import java.util.Date;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduleThreadPoolTest {
public static void main(String[] args) {
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
for (int i=0;i<2;i++){
scheduledThreadPool.scheduleAtFixedRate(new Task("task-"+i),0,2, TimeUnit.SECONDS);
}
}
}
class Task implements Runnable{
private String name;
public Task(String name) {
this.name = name;
}
public void run() {
try {
System.out.println("name="+name+",startTime="+new Date());
Thread.sleep(3000);
System.out.println("name="+name+",endTime="+new Date());
//线程池执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
四、定时任务框架之quartz
官网地址:http://www.quartz-scheduler.org/
@DisallowConcurrentExecution:禁止并发地执行同一个job定义(jobDetail定义)的多个实例。
@PersistJobDataAfterExecution:持久化jobDetail中的jobDateMap(对trigger中的datemap无效)。
package com.tuling.quartz;
import org.quartz.*;
import java.util.Date;
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
public class MyJob implements Job {
private String name;
public void setName(String name) {
this.name = name;
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
/*JobDataMap jobDetailMap = context.getJobDetail().getJobDataMap();
JobDataMap triggerMap = context.getTrigger().getJobDataMap();
JobDataMap mergeMap = context.getMergedJobDataMap();
System.out.println("jobDetailMap:"+jobDetailMap.getString("job"));
System.out.println("triggerMap:"+triggerMap.getString("trigger"));
System.out.println("mergeMap:"+mergeMap.getString("trigger"));
System.out.println("name:"+name);*/
/*System.out.println("jobDetail:"+System.identityHashCode(context.getJobDetail()));
System.out.println("job:"+System.identityHashCode(context.getJobInstance()));*/
/*System.out.println("execute:"+new Date());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
JobDataMap triggerMap = context.getTrigger().getJobDataMap();
JobDataMap jobDetailMap = context.getJobDetail().getJobDataMap();
triggerMap.put("count",triggerMap.getInt("count")+1);
jobDetailMap.put("count1",jobDetailMap.getInt("count1")+1);
System.out.println("triggerMap count:"+triggerMap.getInt("count"));
System.out.println("jobDetailMap count:"+jobDetailMap.getInt("count1"));
}
}
package com.tuling.quartz;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
public class TestJob {
public static void main(String[] args) {
JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
.withIdentity("job1","group1")
.usingJobData("job","jobDetail")
.usingJobData("name","jobDetail")
.usingJobData("count1",0)
.build();
int count=0;
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1","trigger1")
.usingJobData("trigger","trigger")
.usingJobData("count",count)
//.usingJobData("name","trigger")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1)
.repeatForever())
.build();
try {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.scheduleJob(jobDetail,trigger);
scheduler.start();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
五、触发器
优先级:同时触发的trigger之间才会比较优先级; 如果trigger是可恢复的,在恢复后再调度时,优先级不变。
calendar:用于排除时间段。
misfire(错过触发/失火):
判断条件:job到达触发时间时没有被执行;
被执行的延迟时间超过了quartz配置的misfireThreshold阈值。
产生原因:在job需要触发的时间点,scheduler停止了;
当job达到触发时间时,所有线程都被其他job占用,没有可用线程;
job使用了@DisallowConcurrentExecution注解,job不能并发执行,当达到下一个job执行点的时候,上一个任务还没有完成;
job指定了过去的开始执行时间。
策略: 默认使用MISFIRE_INSTRUCTION_SMART_POLICY策略。
SimpleTrigger:
now*相关策略,会立即执行第一个misfire的任务,同时会修改startTime和repeatCount,因此会重新计算finalFireTime,原计划执行时间会被打乱。
next*相关策略,不会立即执行misfire的任务,也不会修改startTime和repeatCount,因此finalFireTime也没有改变,发生了misfire也还是按照原计划进行执行。
CronTrigger:
MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY:quartz不会判断发生了misfire,立即执行所有发生了misfire的任务,然后按照原计划进行执行。
例如:10:15分立即执行9:00和10:00的任务,然后等待下一个任务在11:00执行,后续按照原计划执行。
MISFIRE_INSTRUCTION_DO_NOTHING:所有发生misfire的任务都被忽略,只是按照原计划继续执行。
MISFIRE_INSTRUCTION_FIRE_NOW:立即执行第一个发生misfire的任务,忽略其他发生misfire的任务,然后按照原计划继续执行。
例如:在10:15立即执行9:00任务,忽略10:00任务,然后等待下一个任务在11:00执行,后续按照原计划执行。
六、CronExpression
秒 | 分 | 时 | 日 | 月 | 星期 | 年 | |
---|---|---|---|---|---|---|---|
允许的符号 | , - * / | , - * / | , - * / | , - * / ? L W | , - * / | , - * / ? L # | , - * / |
允许的值 | 0-59 | 0-59 | 0-23 | 1-31 | 0-11 或 JAN-DEC | 1-7 或 SUN-SAT | 1970-2199 |
(1):表示匹配该域的任意值。假如在Minutes域使用, 即表示每分钟都会触发事件。
(2)?:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和DayofWeek会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 * ?, 其中最后一位只能用?,而不能使用*,如果使用*表示不管星期几都会触发,实际上并不是这样。
(3)-:表示范围。例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次
(4)/:表示起始时间开始触发,然后每隔固定时间触发一次。例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次.
(5),:表示列出枚举值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。
(6)L:表示最后,只能出现在DayofWeek和DayofMonth域。如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。
(7)W:表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份 。
(8)LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。
(9)#:用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三。