在前面的一篇文章《java.util.Timer使用详解及注意事项》中讲到了使用java.util.Timer实现定时任务,其中也讲到了使用java.util.Timer存在一些缺陷和不足。从JDK1.5开始,JDK中增加了接口java.util.concurrent.ScheduledExecutorService,我们可以使用ScheduledExecutorService来实现比java.util.Timer更安全更优雅的定时任务。
java.util.Timer的schedule方法未按预期执行
假设有两个定时任务,一个定时任务执行需要花费1000ms,另一个定时任务执行需要花费5000ms,使用java.util.Timer的schedule方法实现,如下:
package cn.lovecto.test;
import java.util.Timer;
import java.util.TimerTask;
public class TimerJob {
private static Timer timer = new Timer(TimerJob.class.getName(), true);
private static long JOB_1_TIME = System.currentTimeMillis();
private static long JOB_2_TIME = System.currentTimeMillis();
static {
//每1000ms执行一次
timer.schedule(new TimerTask() {
@Override
public void run() {
doSomething1();
}
}, 0, 1000);
//每5000执行一次
timer.schedule(new TimerTask() {
@Override
public void run() {
doSomething2();
}
}, 0, 5000);
}
/**
* 这个方法执行时间大概是1000ms
*/
private static void doSomething1() {
long curent = System.currentTimeMillis();
long period = curent - JOB_1_TIME;
JOB_1_TIME = curent;
System.out.println("doSomething1,period=" + period);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 这个方法执行时间大概是5000ms
*/
private static void doSomething2() {
long curent = System.currentTimeMillis();
long period = curent - JOB_2_TIME;
JOB_2_TIME = curent;
System.out.println("doSomething2,period=" + period);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
//循环查看执行效果
while (true) {
Thread.sleep(100);
}
}
}
执行打印结果如下:
doSomething1,period=0
doSomething2,period=1000
doSomething1,period=6000
doSomething2,period=6000
doSomething1,period=6000
doSomething2,period=6000
doSomething1,period=6001
doSomething2,period=6002
doSomething1,period=6001
doSomething2,period=6000
doSomething1,period=6000
doSomething2,period=6000
根据运行结果分析,第一个任务的doSomething1执行时间大概1000ms,设定任务的period也是1000ms,但预期的结果两次执行时间平均是6000ms;第二个任务的doSomething2执行时间大概是5000ms,设定任务的period也是5000ms,但预期结果两次执行时间平均也是6000ms。导致这个问题的原因就是java.util.Timer的内部实现只有一个线程,任务队列有多个时,一个任务的执行时间将会影响到其他任务的执行时间的精确性。
java.util.Timer的scheduleAtFixedRate方法未按预期执行
现在我们把上面的代码修改下,把其中schedule的部分修改为scheduleAtFixedRate,其他地方保持原来的逻辑:
static {
//每1000ms执行一次
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
doSomething1();
}
}, 0, 1000);
//每5000执行一次
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
doSomething2();
}
}, 0, 5000);
}
运行打印结果如下:
doSomething1,period=1
doSomething2,period=1001
doSomething1,period=6001
doSomething1,period=1000
doSomething1,period=1000
doSomething1,period=1000
doSomething1,period=1000
doSomething2,period=10001
doSomething1,period=6000
doSomething1,period=1000
doSomething1,period=1001
doSomething1,period=1000
doSomething1,period=1000
doSomething2,period=10001
doSomething1,period=6001
doSomething1,period=1000
doSomething1,period=1000
doSomething1,period=1000
doSomething1,period=1000
doSomething2,period=10001
根据运行结果分析,任务一执行一次后,紧接着任务二执行,任务一第二次执行是6秒之后了,紧接着任务一会再执行4次,任务二再执行,任务二的两次执行时间差平均在10秒。虽然scheduleAtFixedRate相比schedule增加了较短任务执行时间的任务的执行机会,但还是影响了各个任务的执行时间精确性。
所以java.util.Timer适用于只有一个任务,或者多个任务执行时间上并没有太大冲突的情况下,所以一般不建议使用java.util.Timer。JDK1.5后我们多了一种更优雅的选择java.util.concurrent.ScheduledExecutorService。
使用ScheduledExecutorService实现定时任务
同样使用上面的例子,我们使用java.util.concurrent.ScheduledExecutorService的方式来实现:
package cn.lovecto.test;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledExecutorServiceJob {
private static ScheduledExecutorService scheduledExecutorService = Executors
.newScheduledThreadPool(2);
private static long JOB_1_TIME = System.currentTimeMillis();
private static long JOB_2_TIME = System.currentTimeMillis();
static {
// 每1000ms执行一次
scheduledExecutorService.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
doSomething1();
}
}, 0, 1000, TimeUnit.MILLISECONDS);
// 每5000ms执行一次
scheduledExecutorService.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
doSomething2();
}
}, 0, 5000, TimeUnit.MILLISECONDS);
}
/**
* 这个方法执行时间大概是1000ms
*/
private static void doSomething1() {
long curent = System.currentTimeMillis();
long period = curent - JOB_1_TIME;
JOB_1_TIME = curent;
System.out.println("doSomething1,period=" + period);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 这个方法执行时间大概是5000ms
*/
private static void doSomething2() {
long curent = System.currentTimeMillis();
long period = curent - JOB_2_TIME;
JOB_2_TIME = curent;
System.out.println("doSomething2,period=" + period);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
// 循环查看执行效果
while (true) {
Thread.sleep(100);
}
}
}
此处使用的是scheduleAtFixedRate方法,执行结果如下:
doSomething1,period=4
doSomething2,period=4
doSomething1,period=1000
doSomething1,period=1000
doSomething1,period=1000
doSomething1,period=1000
doSomething2,period=5001
doSomething1,period=1001
doSomething1,period=1000
doSomething1,period=1000
doSomething1,period=1000
doSomething1,period=1000
doSomething2,period=5000
分析执行结果,基本达到预期。但如果把上面添加任务的地方换成scheduleWithFixedDelay:
static {
// 每1000ms执行一次
scheduledExecutorService.scheduleWithFixedDelay(new TimerTask() {
@Override
public void run() {
doSomething1();
}
}, 0, 1000, TimeUnit.MILLISECONDS);
// 每5000ms执行一次
scheduledExecutorService.scheduleWithFixedDelay(new TimerTask() {
@Override
public void run() {
doSomething2();
}
}, 0, 5000, TimeUnit.MILLISECONDS);
}
执行结果如下:
doSomething2,period=2
doSomething1,period=2
doSomething1,period=2001
doSomething1,period=2001
doSomething1,period=2000
doSomething1,period=2001
doSomething2,period=10001
doSomething1,period=2001
doSomething1,period=2001
doSomething1,period=2004
doSomething1,period=2000
doSomething1,period=2001
doSomething2,period=10000
分析运行结果,虽然每个任务的运行频率基本相同,但跟scheduleAtFixedRate的运行结果却有差别。原因是scheduleAtFixedRate是以上一次任务的开始时间为间隔的,并且当任务执行时间大于设置的间隔时间时,真正间隔的时间由任务执行时间为准;而scheduleWithFixedDelay是以上一次任务的结束时间为间隔的。
我们拿任务一的doSomething1举例,由于scheduleAtFixedRate是以两次任务的开始时间为间隔,所以平均每间隔1000ms会打印一次;scheduleWithFixedDelay是以结束时间和下一次的开始时间为间隔的,doSomething1执行时间1000ms,间隔是1000ms,所以平均每隔2000ms会打印一次。
总结
使用java.util.concurrent.ScheduledExecutorService完全能够胜任java.util.Timer所具备的功能,且java.util.concurrent.ScheduledExecutorService的默认实现是线程池,能够弥补java.util.Timer暴露出来的缺陷。所以使用ScheduledExecutorService替代java.util.Timer实现更优雅的定时任务吧。虽然本例没有讲到异常的情况,但务必在被调度的任务上加上异常捕获,因为异常可能会影响任务接下来的执行。