定时器:Timer类
常用方法方法:
1.schedule(TimeTask timetask,long delay,(long period)):
TimeTask:实现了Runnable类,实现时需要重写run方法
delay:表示延迟多少(decay)后开始执行任务,单位是毫秒,这个参数也可以是日期(Date)
period:周期时间,表示定时器循环执行任务之间的间隔时间,时间是毫秒
如果没有period参数,那么就只执行一次任务内容
2.scheduleAtFixedRate(TimeTask timetask,long delay,long period):
参数和schedule的参数相同,其作用为定时器设置循环执行的内容,第一次执行内容的延迟时间,循环的周期时间。可以看出schedule方法的功能其实已经包括这个方法了
3.cancel():关闭计时器
schedule(TimeTask timetask,long delay)
public static void main(String[] args) { System.out.println("任务三秒后开启"); Timer t = new Timer(); //定时执行任务 表示几秒后执行run方法里面的内容 t.schedule(new TimerTask() { @Override public void run() { System.out.println("任务开启"); } },3000); }
schedule(TimeTask timetask,long delay,long period):
public static void main(String[] args) { System.out.println("任务三秒后开启"); Timer t = new Timer(); //定时执行任务 表示几秒后执行run方法里面的内容 t.schedule(new TimerTask() { @Override public void run() { System.out.println("开始以1秒的时间间隔循环执行任务"); } },2000,1000); }
scheduleAtFixedRate(TimeTask timetask,long delay,long period):
public static void main(String[] args) { System.out.println("任务两秒后开启"); Timer t = new Timer(); //定时执行任务 表示几秒后执行run方法里面的内容 t.schedule(new TimerTask() { @Override public void run() { System.out.println("任务开启"); } },2000); t.scheduleAtFixedRate(new TimerTask() { @Override public void run() { System.out.println("开始循环任务(间隔1s)"); } },2000,1000); }
指定定时器执行固定个任务后结束
public static void main(String[] args) { Timer t = new Timer(); int timeCount = 5; long delay = 1000; long period = 1000; t.schedule(new TimerTask() { int count = 1; @Override public void run() { if(count >= timeCount){ t.cancel(); } System.out.println("执行任务:"+count); count++; } },delay,period); }
模拟实现定时器
import java.util.concurrent.BlockingQueue; import java.util.concurrent.PriorityBlockingQueue; class Task implements Comparable<Task>{ //这个类用来描述任务的 private Runnable runnable; //执行的任务时间 private long time; //time + System.currentTimeMillis() public Task(Runnable runnable,long time){ this.runnable = runnable; this.time = time; } public long getTime(){ return time ; } public void run() { runnable.run(); } //比较规则 执行时间在前的先执行 @Override public int compareTo(Task o) { return (int)(this.time - o.time); } } public class MyTimer{ //一个阻塞队列 private BlockingQueue<Task> queue = new PriorityBlockingQueue<>(); //扫描线程 private Thread t = null; public MyTimer(){ t = new Thread(() -> { while(true) { try { Task task = queue.take(); if (task.getTime() > System.currentTimeMillis()) { //还没到时间 queue.put(task); } else { //执行任务 task.run(); } } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); } public void schedule(Runnable runnable,long time) throws InterruptedException { //在这一步加System.currentTimeMillis() 不能在getTime方法中加,不然就会一直大于System.currentTimeMillis() Task task = new Task(runnable, time+System.currentTimeMillis()); queue.put(task); } public static void main(String[] args) throws InterruptedException { MyTimer m = new MyTimer(); m.schedule(new Runnable() { @Override public void run() { System.out.println("这是任务1"); } },2000); m.schedule(new Runnable() { @Override public void run() { System.out.println("这是任务2"); } },1000); } }
问题:
1.忙等
假设此时时间是上午9点,第一个任务的时间是上午10点,那么在这一个小时内,程序会一直重复执行一个代码:
Task task = queue.take(); if (task.getTime() > System.currentTimeMillis()) { //还没到时间 queue.put(task); }
这行代码可能会循环个非常多亿次,且别忘了,优先级队列的底层是用堆实现的,每当我们取出一个元素又出现插入时,根据堆的调整,此元素又会在堆顶,而每一层调整都是有开销的,故这一时间段的开销是重复且多余的,所以我们就等一次,等堆顶任务执行时间与当前时间的差值就可,修改代码:
Task task = queue.take(); if (task.getTime() > System.currentTimeMillis()){ //还没到时间 queue.put(task); Thread.sleep(task.getTime() - System.currentTimeMillis());
大伙们是不是觉得此时的代码就已经完美了?!!
然而并不是! 我们并不能使用sleep休眠来休眠两时间差
试想一下,如果咱们休眠的时候突然插进来了一个上午9.30执行的任务,那么这个任务就不会被执行到!故使用带参版本的wait()方法才是最好的选择
再次修改代码:
if (task.getTime() > System.currentTimeMillis()){ //还没到时间 queue.put(task); synchronized (this){ this.wait(task.getTime() - System.currentTimeMillis()); } public void schedule(Runnable runnable,long time) throws InterruptedException { Task task = new Task(runnable, time+System.currentTimeMillis()); queue.put(task); //唤醒t线程里的wait操作 this.notify(); }
这个时候代码看起来是不是似乎万无一失了已经!
but,我们来考虑一个极端极端极端极端的情况
2.极端情况
if (task.getTime() > System.currentTimeMillis()){ //还没到时间 queue.put(task); synchronized (this){ this.wait(task.getTime() - System.currentTimeMillis()); }
如果线程1在执行到queue.put(task)时,恰好被调度走了,此时另一个线程调用schedule方法,且如果当线程2的任务时间小于线程1的任务时间时,此时线程2的任务就不会执行到。因为线程2的notify没作用,线程1都还没有执行到wait方法,但线程1重新执行时,此时的时间差已经是固定了的,但是这个时间差要大于线程2任务执行的时间,故线程2就会被"极端"地错过
完美代码
加大锁的力度:造成此极端情况的原因即为:take和wait是多步操作,非原子性
public MyTimer(){ t = new Thread(() -> { while(true) { synchronized (this){ try { Task task = queue.take(); if (task.getTime() > System.currentTimeMillis()) { //还没到时间 queue.put(task); this.wait(task.getTime() - System.currentTimeMillis()); } else { //执行任务 task.run(); } } catch (InterruptedException e) { e.printStackTrace(); } } } }); t.start(); }