概述
文章目录
- 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点去执行定时任务
Timer 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的上限。
/**
* timer的任务队列,这个队列共享给TimerThread ,timer.schedule...()生产任务,
* TimerThread 消费任务,在适合的时候执行该任务,过时了则从队列移除
*/
private final TaskQueue queue = new TaskQueue();
//消费者线程
private final TimerThread thread = new TimerThread(queue);
下面看生产者的核心方法,所有生产者最终都会走到这个方法
生产者代码
private 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();//唤醒堆顶任务
}
}
任务状态
//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)代码
void 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)代码
private 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退出循环,已经是最小堆
至此生产者的代码就看完了
消费者代码
//单消费者
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();代码
void removeMin() {
queue[1] = queue[size];//堆顶置为堆尾
queue[size--] = null; //堆尾置为null,size--
fixDown(1);//对堆顶进行一次调整,和fixup反着来,目的都是为了调整成最小堆
}
timer的schedule和scheduleAtFixedRate区别:
Timer 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的缺点:
缺点一:对于耗时任务及多任务非常不友好
Timer 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只适用于耗时短的单任务。
缺点二:对于运行时异常不友好
Timer 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详解所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复