前言
在作后台任务的时候常常须要实现各类各类的定时的,周期性的任务。好比每隔一段时间更新一下缓存之类的。一般周期性的任务均可以使用以下方式实现:html
- class MyTimerThread extends Thread {
- @Override
- public void run() {
- while(true) {
- try {
- Thread.sleep(60*1000);
-
- //每隔1分钟须要执行的任务
- doTask();
-
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- };
- }
class MyTimerThread extends Thread {
@Override
public void run() {
while(true) {
try {
Thread.sleep(60*1000);
//每隔1分钟须要执行的任务
doTask();
} catch (Exception e) {
e.printStackTrace();
}
}
};
}
其实用这种方式我还没遇到过什么问题。网上有人说调用线程sleep()方法会致使线程休眠时仍是会占用cpu资源不释放(而wait()不会),这种说法应该是不正确的。如有人知道其中存在的问题,敬请告知!因为这种实现通常都是一个线程对于一个定时任务,且没有实如今指定时间启动任务(也能够实现,加个时间判断就能够了)。
Timer简介
JDK提供的Timer是很经常使用的定时任务调度器。在说到timer的原理时,咱们先看看Timer里面的一些常见方法:
- /**
- * 这个方法是调度一个task,通过delay(ms)后开始进行调度,仅仅调度一次
- */
- public void schedule(TimerTask task, long delay)
-
- /**
- * 在指定的时间点time上调度一次
- */
- public void schedule(TimerTask task, Date time)
- 在指定的时间点time上调度一次。
-
- /**
- * 周期性调度任务,在delay(ms)后开始调度。
- * 而且任务开始时间的间隔为period(ms),即“固定间隔”执行
- */
- public void schedule(TimerTask task, long delay, long period)
-
- /**
- * 和上一个方法相似,惟一的区别就是传入的第二个参数为第一次调度的时间
- */
- public void schedule(TimerTask task, Date firstTime, long period)
-
-
- public void scheduleAtFixedRate(TimerTask task, long delay, long period)
-
- public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period)
/**
* 这个方法是调度一个task,通过delay(ms)后开始进行调度,仅仅调度一次
*/
public void schedule(TimerTask task, long delay)
/**
* 在指定的时间点time上调度一次
*/
public void schedule(TimerTask task, Date time)
在指定的时间点time上调度一次。
/**
* 周期性调度任务,在delay(ms)后开始调度。
* 而且任务开始时间的间隔为period(ms),即“固定间隔”执行
*/
public void schedule(TimerTask task, long delay, long period)
/**
* 和上一个方法相似,惟一的区别就是传入的第二个参数为第一次调度的时间
*/
public void schedule(TimerTask task, Date firstTime, long period)
public void scheduleAtFixedRate(TimerTask task, long delay, long period)
public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period)
不过比较很差理解的是Timer中,存在schedule和scheduleAtFixedRate两套不一样调度算法的方法,
它们的共同点是若判断理论执行时间小于实际执行时间时,都会立刻执行任务,区别在于计算下一次执行时间的方式不一样:
schedule: 任务开始的时间 + period(时间片断),强调“固定间隔”地执行任务
scheduleAtFixedRate: 参数设定开始的时间 + period(时间片断),强调“固定频率”地执行任务
能够看出前者采用实际值,后者采用理论值。不过实际上若参数设定的开始时间比当前时间大的话,二者执行的效果是同样的。举个反例说明:
- public static void main(String[] args) {
-
- TimerTask task = new TimerTask() {
- @Override
- public void run() {
- System.out.println(”do task…….”);
- }
- };
-
- SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”);
- Timer timer = new Timer();
- try {
-
- timer.schedule(task, sdf.parse(”2016-4-9 00:00:00”), 5000);
-
- //timer.scheduleAtFixedRate(task, sdf.parse(“2016-4-9 00:00:00”),5000);
-
- } catch (ParseException e) {
- e.printStackTrace();
- }
- }
public static void main(String[] args) {
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println("do task.......");
}
};
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Timer timer = new Timer();
try {
timer.schedule(task, sdf.parse("2016-4-9 00:00:00"), 5000);
//timer.scheduleAtFixedRate(task, sdf.parse("2016-4-9 00:00:00"),5000);
} catch (ParseException e) {
e.printStackTrace();
}
}
以上是参数设定时间比当前时间小的状况,我在2016-4-9 00:00:20时才启动上面的程序:
对于schedule,打印了1条”do task”。由于理论执行时间(00:00:00)小于实际执行时间(00:00:20)。而后等,由于下一次执行的时间为00:00:25。
对于scheduleAtFixedRate,打印了4条”do task”。由于它的理论执行时间分别是00:00:0五、00:00:十、00:00:1五、00:00:20、00:00:25……如今知道固定频率的意思了吧!说好了要执行多少次就是多少次。
Timer的缺陷
Timer被设计成支持多个定时任务,经过源码发现它有一个任务队列用来存放这些定时任务,而且启动了一个线程来处理,以下部分源码所示:
- public class Timer {
-
- // 任务队列
- private final TaskQueue queue = new TaskQueue();
-
- // 处理线程
- private final TimerThread thread = new TimerThread(queue);
public class Timer {
// 任务队列
private final TaskQueue queue = new TaskQueue();
// 处理线程
private final TimerThread thread = new TimerThread(queue);
经过这种单线程的方式实现,在存在多个定时任务的时候便会存在问题:
若任务B执行时间过长,将致使任务A延迟了启动时间!
还存在另一个问题,应该是属于设计的问题:
若任务线程在执行队列中某个任务时,该任务抛出异常,将致使线程因跳出循环体而终止,即Timer中止了工做!
一样是举个栗子:
- public static void main(String[] args) {
-
- Timer timer = new Timer();
-
- timer.schedule(new TimerTask() {
- @Override
- public void run() {
- SimpleDateFormat sdf = new SimpleDateFormat(“HH:mm:ss”);
- System.out.println(sdf.format(new Date()) + “ A: do task”);
- }
- }, 0, 5*1000);
-
- timer.schedule(new TimerTask() {
- @Override
- public void run() {
- SimpleDateFormat sdf = new SimpleDateFormat(“HH:mm:ss”);
- System.out.println(sdf.format(new Date()) + “ B: sleep”);
- try {
- Thread.sleep(20*1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }, 10*1000, 5000);
-
- timer.schedule(new TimerTask() {
- @Override
- public void run() {
- SimpleDateFormat sdf = new SimpleDateFormat(“HH:mm:ss”);
- System.out.println(sdf.format(new Date()) + “ C: throw Exception”);
- throw new RuntimeException(“test”);
- }
- }, 30*1000, 5000);
- }
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println(sdf.format(new Date()) + " A: do task");
}
}, 0, 5*1000);
timer.schedule(new TimerTask() {
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println(sdf.format(new Date()) + " B: sleep");
try {
Thread.sleep(20*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 10*1000, 5000);
timer.schedule(new TimerTask() {
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println(sdf.format(new Date()) + " C: throw Exception");
throw new RuntimeException("test");
}
}, 30*1000, 5000);
}
经过以上程序发现:一开始,任务A能正常每隔5秒运行一次。在任务B启动后,因为任务B运行时间须要20秒,致使任务A要等到任务B执行完才能执行。更可怕的是,任务C启动后,抛了个异常,定时任务挂了!
不过这种单线程的实现也有优势:线程安全!
ScheduledThreadPoolExecutor简介
ScheduledThreadPoolExecutor能够说是Timer的多线程实现版本,连JDK官方都推荐使用ScheduledThreadPoolExecutor替代Timer。它是接口ScheduledExecutorService的子类,主要方法说明以下:
- /**
- * 调度一个task,通过delay(时间单位由参数unit决定)后开始进行调度,仅仅调度一次
- */
- public ScheduledFuture<?> schedule(Runnable command,
- long delay, TimeUnit unit);
-
- /**
- * 同上,支持参数不同
- */
- public <V> ScheduledFuture<V> schedule(Callable<V> callable,
- long delay, TimeUnit unit);
-
- /**
- * 周期性调度任务,在delay后开始调度,适合执行时间比“间隔”短的任务
- * 而且任务开始时间的间隔为period,即“固定间隔”执行。
- * 若是任务执行的时间比period长的话,会致使该任务延迟执行,不会同时执行!
- * 若是任务执行过程抛出异常,后续不会再执行该任务!
- */
- public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
- long initialDelay ,long period ,TimeUnit unit);
-
- /**
- * Timer所没有的“特点”方法,称为“固定延迟(delay)”调度,适合执行时间比“间隔”长的任务
- * 在initialDelay后开始调度该任务
- * 随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟period
- * 即下一次任务开始的时间为:上一次任务结束时间(而不是开始时间) + delay时间
- * 若是任务执行过程抛出异常,后续不会再执行该任务!
- */
- public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
- long initialDelay ,long delay ,TimeUnit unit);
/**
* 调度一个task,通过delay(时间单位由参数unit决定)后开始进行调度,仅仅调度一次
*/
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
/**
* 同上,支持参数不同
*/
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay, TimeUnit unit);
/**
* 周期性调度任务,在delay后开始调度,适合执行时间比“间隔”短的任务
* 而且任务开始时间的间隔为period,即“固定间隔”执行。
* 若是任务执行的时间比period长的话,会致使该任务延迟执行,不会同时执行!
* 若是任务执行过程抛出异常,后续不会再执行该任务!
*/
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay ,long period ,TimeUnit unit);
/**
* Timer所没有的“特点”方法,称为“固定延迟(delay)”调度,适合执行时间比“间隔”长的任务
* 在initialDelay后开始调度该任务
* 随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟period
* 即下一次任务开始的时间为:上一次任务结束时间(而不是开始时间) + delay时间
* 若是任务执行过程抛出异常,后续不会再执行该任务!
*/
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay ,long delay ,TimeUnit unit);
ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,因此本质上说ScheduledThreadPoolExecutor仍是一个线程池(可参考
《Java线程池ThreadPoolExecutor简介》)。它也有coorPoolSize和workQueue,接受Runnable的子类做为任务。
特殊的地方在于它实现了本身的工做队列DelayedWorkQueue,该任务队列的做用是按照必定顺序对队列中的任务进行排序。好比,按照距离下次执行时间的长短的升序方式排列,让须要尽快执行的任务排在队首,“不那么着急”的任务排在队列后方,从而方便线程获取到“应该”被执行的任务。除此以外,ScheduledThreadPoolExecutor还在任务执行结束后,计算出下次执行的时间,从新放到工做队列中,等待下次调用。
上面经过一个程序说明了Timer存在的问题!这里我将Timer换成了用ScheduledThreadPoolExecutor来实现,注意TimerTask也是Runnable的子类。
- public static void main(String[] args) {
- int corePoolSize = 3;
- ScheduledExecutorService pool = Executors.newScheduledThreadPool(corePoolSize);
-
- pool.scheduleAtFixedRate(new TimerTask() {
- @Override
- public void run() {
- SimpleDateFormat sdf = new SimpleDateFormat(“HH:mm:ss”);
- System.out.println(sdf.format(new Date()) + “ A: do task”);
- }
- }, 0 ,5, TimeUnit.SECONDS);
-
- pool.scheduleAtFixedRate(new TimerTask() {
- @Override
- public void run() {
- SimpleDateFormat sdf = new SimpleDateFormat(“HH:mm:ss”);
- System.out.println(sdf.format(new Date()) + “ B: sleep”);
- try {
- Thread.sleep(20*1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }, 10, 5, TimeUnit.SECONDS);
-
- pool.scheduleAtFixedRate(new TimerTask() {
- @Override
- public void run() {
- SimpleDateFormat sdf = new SimpleDateFormat(“HH:mm:ss”);
- System.out.println(sdf.format(new Date()) + “ C: throw Exception”);
- throw new RuntimeException(“test”);
- }
- }, 30, 5, TimeUnit.SECONDS);
- }
public static void main(String[] args) {
int corePoolSize = 3;
ScheduledExecutorService pool = Executors.newScheduledThreadPool(corePoolSize);
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println(sdf.format(new Date()) + " A: do task");
}
}, 0 ,5, TimeUnit.SECONDS);
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println(sdf.format(new Date()) + " B: sleep");
try {
Thread.sleep(20*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 10, 5, TimeUnit.SECONDS);
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println(sdf.format(new Date()) + " C: throw Exception");
throw new RuntimeException("test");
}
}, 30, 5, TimeUnit.SECONDS);
}
因为有3个任务须要调度,所以我将corePoolSize设置为3。经过控制台打印能够看到此次任务A一直都在正常运行(任务时间间隔为5秒),并不受任务B的影响。任务C抛出异常后,虽然自己中止了调度,但没有影响到其余任务的调度。能够说ScheduledThreadPoolExecutor解决Timer存在的问题!
那要是将corePoolSize设置为1,变成单线程跑呢?结果固然是和Timer同样,任务B会致使任务A延迟执行,不过比较好的是任务C抛异常不会影响到其余任务的调度。
能够说ScheduledThreadPoolExecutor适用于大部分场景,甚至就算timer提供的Date参数类型的开始时间也能够经过本身转的方式来实现。任务调度框架Quatz也是在ScheduledThreadPoolExecutor基础上实现的。
通常咱们都使用单线程版的ScheduledThreadPoolExecutor居多,推荐经过如下方式来构建(构建后其线程数就不可更改):
- ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();
ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();
总结
不少时候真的不可能记得住这些类库的特性,一不当心就会踩坑!好比我上面反复强调的要是任务执行过程抛出异常了会怎么怎么样,其实人家的API注释是有说明的。另外是不肯定的仍是用经过写demo来实践一下,看看是否是真的这样!还有就是除了看资料,写demo,还能够了解底层实现,这样了解得更透彻。好比在若只有一个任务须要调度的状况下,其实就算用Timer也是能够的。
如上文有不正确的地方,感谢指点出来!
参考