文章目录
- timer介绍:
- timer的使用:
- timer源码分析:
- 生产者代码
- 消费者代码
- timer的schedule和scheduleAtFixedRate区别:
- timer的缺点:
- timer的替代产品:
timer介绍:
Timer是Josh Bloch在jdk1.3发布的一个新的api,主要用于做定时任务.
timer的使用:
1:schedule(TimerTask task, long delay) 在delay毫秒的延迟后执行task
2:schedule(TimerTask task, Date time) 在指定的time时间执行task
3:schedule(TimerTask task, long delay, long period) 在delay毫秒延迟后按照period的周期循环定时执行task
4:schedule(TimerTask task, Date firstTime, long period)在指定的firstTime时间开始按照period的周期循环定时执行task
5:scheduleAtFixedRate(TimerTask task, long delay, long period) 这个先理解为和3一样,后面会解释二者的区别
6:scheduleAtFixedRate(TimerTask task, Date firstTime, long period)这个先理解为和4一样,后面会解释二者的区别
6的实例:每天24点去执行定时任务
1
2
3
4
5
6
7
8
9
10
11
12Timer timer = new Timer(); Calendar cal = Calendar.getInstance(); cal.set(Calendar.HOUR_OF_DAY,24); cal.set(Calendar.MINUTE,0); cal.set(Calendar.SECOND,0); timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { System.out.println("执行任务"); } },cal.getTime(),24*60*60*1000);
timer源码分析:
TimerThread :单线程/消费者/任务线程
TaskQueue :任务队列/优先队列/最小平衡堆/容器
我们写的定时任务是生产者,TimerThread 是消费者,可以看出这是一个单消费者多生产者的模型,而且这个线程还是采取轮询的方式来消费产品,这两个模型决定了Timer的上限。
1
2
3
4
5
6
7
8/** * timer的任务队列,这个队列共享给TimerThread ,timer.schedule...()生产任务, * TimerThread 消费任务,在适合的时候执行该任务,过时了则从队列移除 */ private final TaskQueue queue = new TaskQueue(); //消费者线程 private final TimerThread thread = new TimerThread(queue);
下面看生产者的核心方法,所有生产者最终都会走到这个方法
生产者代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25private void sched(TimerTask task, long time, long period) { if (time < 0) throw new IllegalArgumentException("Illegal execution time."); //对周期做一下限制 防止溢出 if (Math.abs(period) > (Long.MAX_VALUE >> 1)) period >>= 1; synchronized(queue) { if (!thread.newTasksMayBeScheduled) throw new IllegalStateException("Timer already cancelled."); synchronized(task.lock) { if (task.state != TimerTask.VIRGIN) throw new IllegalStateException( "Task already scheduled or cancelled"); task.nextExecutionTime = time;//这个任务下一次执行时间 task.period = period;//执行周期 task.state = TimerTask.SCHEDULED;//任务状态 } queue.add(task);//加入最小平衡堆 if (queue.getMin() == task)//如果堆顶任务就是刚加进去的任务 queue.notify();//唤醒堆顶任务 } }
任务状态
1
2
3
4
5
6
7
8
9
10//This task has not yet been scheduled. //处女,任务还没有被调度,默认是这个 static final int VIRGIN = 0; //任务被调度,但是未执行,就是说在队列等待调度 static final int SCHEDULED = 1; //已经执行了,或者正在执行 static final int EXECUTED = 2; //任务被取消with a call to TimerTask.cancel static final int CANCELLED = 3;
queue.add(task)代码
1
2
3
4
5
6
7
8
9
10void add(TimerTask task) { // 容器初始大小128,以2为指数扩容 if (size + 1 == queue.length) queue = Arrays.copyOf(queue, 2*queue.length); //size初始为0,可以看出queue的下标0被舍弃掉了,直接从下标1开始入堆 //这样一来i的左孩子就是2*i了,右孩子是2*i+1. queue[++size] = task; fixUp(size);//加入堆中后 可能不是最小堆,所以需要对堆做一次fixup调整为最小平衡堆 }
fixUp(size)代码
1
2
3
4
5
6
7
8
9
10private void fixUp(int k) { while (k > 1) { int j = k >> 1; if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime) break; TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp; k = j; } }
这段代码的意思其实就是把刚刚加入堆的任务安排到合适的地方去。直接将代码不好讲,还是看个实例:没有学过堆的同学建议花两小时学一下,不然可能听不懂。假设某个时刻堆的任务是下图所示,2,3,4这些数字代表nextExecutionTime(下次执行的时间),数字越小说明任务优先级越高。
2
/
3 4
某个时刻来了一个nextExecutionTime=1的任务,此时堆中任务如下图所示。
2(下标1)
/
3 4(下标3)
/
1(下标4)
很明显这已经不是一个最小堆了,我们需要把1往上调整。
现在来看代码:
第一次循环
k=4,j=2
if(3<=1)break;这里不成立,但是如果来了一个任务的nextExecutionTime>=3 这里会直接break掉,因为已经是最小堆
交换1和3
2(下标1)
/
1 4(下标3)
/
3(下标4)
k=2
第二次循环
k=2 ,j =1
if(2<=1)break;这里不成立
交换1和2
1(下标1)
/
2 4(下标3)
/
3(下标4)
k=1退出循环,已经是最小堆
至此生产者的代码就看完了
消费者代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60//单消费者 private void mainLoop() { while (true) {//轮询模式 try { TimerTask task; boolean taskFired; synchronized(queue) { // Wait for queue to become non-empty 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 //这两个时间及其重要 //scheduleAtFixedRate和schedule的区别就体现在这两个时间和+-period上面 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 周期任务 //这里的代码及其经典够味 //schedule的period传的是-period //scheduleAtFixedRate的period是+period //如果是schedule调度,下一次执行时间改为 // currentTime-task.period(当前时间-(-period)) //这里看出来schedule是依据当前时间来调度的 //如果是scheduleAtFixedRate调度,下一次执行时间是 //executionTime + task.period, //这里看出来scheduleAtFixedRate是依据执行时间调度的 //(这个执行时间是我们写代码指定的那个时间) //并且while(true){}保证scheduleAtFixedRate这种调度方式会自动补上之前缺失的任务。 queue.rescheduleMin( task.period<0 ? currentTime - task.period : executionTime + task.period); } } } 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) { } } } }
queue.removeMin();代码
1
2
3
4
5
6void removeMin() { queue[1] = queue[size];//堆顶置为堆尾 queue[size--] = null; //堆尾置为null,size-- fixDown(1);//对堆顶进行一次调整,和fixup反着来,目的都是为了调整成最小堆 }
timer的schedule和scheduleAtFixedRate区别:
1
2
3
4
5
6
7
8
9
10Timer timer = new Timer(); SimpleDateFormat fTime = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); Date date = fTime.parse("2019/7/3 10:50:00"); timer.scheduleAtFixedRate(new TimerTask(){ public void run() { System.out.println("exec task"); } },date,3*60*1000);
程序指定运行时间是2019/7/3 10:50:00,每隔三分钟运行一次
如果我等到2019/7/3 10:55:00 去运行这段程序,即已经过了五分钟了。
使用scheduleAtFixedRate会快速打印两个exec task(第一次2019/7/3 10:50:00,第二次2019/7/3 10:53:00),然后按照2019/7/3 10:56:00–> 2019/7/3 10:59:00这样打印下去。也就是说scheduleAtFixedRate是按照指定的时间开始算,如果程序运行的时间晚于这个指定时间,他会一次性补上之前的任务,然后按照间隔时间去执行。
如果使用schedule他不会补上之前的任务,而且他是按照实际执行程序的时间开始算,也就是说如果2019/7/3 10:55:00用 去schedule运行这段程序那么下一次打印时间将是2019/7/3 10:58:00.
timer的缺点:
缺点一:对于耗时任务及多任务非常不友好
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22Timer timer = new Timer(); final long start = System.currentTimeMillis(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("time1:"+ (System.currentTimeMillis()-start)); try { //模拟耗时操作 Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } },1000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("time2:"+ (System.currentTimeMillis()-start)); } },2000);
我希望的结果是
time1:1001
time2:2001
可实际上由于单线程的原因结果是
time1:1001
time2:4001
由这个例子看出Timer只适用于耗时短的单任务。
缺点二:对于运行时异常不友好
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17Timer timer = new Timer(); final long start = System.currentTimeMillis(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("time1:"+ (System.currentTimeMillis()-start)); throw new RuntimeException(); } },1000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("time2:"+ (System.currentTimeMillis()-start)); } },2000);
在timer1里抛运行时异常会导致time2不可用,这一点问题不大,我们有最佳实践:在run里面手动catch异常进行处理。
timer的替代产品:
相比于timer,在jdk1.5的时候,Doug Lea老先生主笔写了juc新api,线程池。其中的带有调度功能的线程池就可以执行定时任务,而且性能及稳定性更优秀。线程池将在下篇博文讲到。
最后
以上就是深情蚂蚁最近收集整理的关于Timer详解的全部内容,更多相关Timer详解内容请搜索靠谱客的其他文章。
发表评论 取消回复