总所周知,ScheduledThreadPoolExecutor是更优于Timer的JDK定时任务,该类支持多线程执行定时任务,能够保证更加准确的时间间隔。该类有三个核心方法,他们分别是:
- schedule 创建并执行在给定延迟后启用的单次操作。
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit) - scheduleAtFixedRate 创建并执行在给定的初始延迟之后,随后以给定的时间段首先启用的周期性动作
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit) - scheduleWithFixedDelay 创建并执行在给定的初始延迟之后首先启用的定期动作,随后在一个执行的终止和下一个执行的开始之间给定的延迟
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit)
其中schedule方法和scheduleWithFixedDelay完全可以单线程实现,执行效率什么的不必说了,如果非要了解一下,请参考我的上一篇文章:JDK中的定时器
在上一篇文章中,我们做了一个实验,如果用一秒的时间间隔执行需要花费两秒的任务,那么会发生什么?
实验结果是Timer的scheduleAtFixedRate任务间隔大约是两秒,而ScheduledThreadPoolExecutor的scheduleWithFixedDelay时间间隔大约是三秒。
1、因为Timer是单线程,它需要等待前一个任务执行完成才能执行下一个任务,就算计时间隔早就计时结束也不会立刻执行下一个任务,因为它是一个单线程的任务。
2、因为ScheduledThreadPoolExecutor的scheduleWithFixedDelay的设定是等到任务执行结束才会计时,等待计时结束才执行下一次任务。所以scheduleWithFixedDelay的两次任务间隔实际上是任务所花时间+任务时间间隔,用时是三秒是合理的
但是问题是传说中拥有多线程的ScheduledThreadPoolExecutor的scheduleAtFixedRate会是怎样的结果呢?按照网上普遍的多线程的吹嘘,它会不会等到任务间隔时间一到,就立马开启另一个进程来启动下一个任务,从而完美保证任务间隔呢?
其实不然,它的实验结果和Timer基本一致:
@Test
public void executorTimer2() throws IOException
{
// TODO code application logic here
//构造一个ScheduledThreadPoolExecutor对象,并且设置它的容量为5个
stpe = new ScheduledThreadPoolExecutor(5);
//隔2秒后开始执行任务,并且在上一次任务开始后隔1秒再执行一次;
stpe.scheduleAtFixedRate(new Runnable()
{
@Override
public void run()
{
index++;
System.out.println("executor= " + getTimes()+" " +index);
try {
//模拟任务执行需要2秒的时间
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(index >=10){
stpe.shutdown();
if(stpe.isShutdown()){
System.out.println("停止了????");
}
}
}
}, 2, 1, TimeUnit.SECONDS);
System.in.read();
}
执行结果:
executor= 2020-12-29 22:09:12 Tue 1
executor= 2020-12-29 22:09:14 Tue 2
executor= 2020-12-29 22:09:16 Tue 3
executor= 2020-12-29 22:09:18 Tue 4
executor= 2020-12-29 22:09:20 Tue 5
executor= 2020-12-29 22:09:22 Tue 6
executor= 2020-12-29 22:09:24 Tue 7
executor= 2020-12-29 22:09:26 Tue 8
executor= 2020-12-29 22:09:28 Tue 9
executor= 2020-12-29 22:09:30 Tue 10
停止了????
可以看到,它和timer的scheduleAtFixedRate一样,虽然设置了一秒的时间间隔,但是却实际上间隔两秒。是不是瞬间感觉被骗了?其实又不然!这是以前中文文档的翻译问题,人家压根就没说当任务时长大于任务间隔的时候会多线程执行任务,且看官方解释:
创建并执行在给定的初始延迟之后,随后以给定的时间段首先启用的周期性动作; 那就是执行将在initialDelay之后开始,然后initialDelay+period ,然后是initialDelay + 2 * period ,等等。 如果任务的执行遇到异常,则后续的执行被抑制。 否则,任务将仅通过取消或终止执行人终止。 如果任务执行时间比其周期长,则后续执行可能会迟到,但不会同时执行。
傻了吧,如果任务执行时间比其周期长,则后续执行可能会迟到,但不会同时执行。
相信这时候拍案而起,准备弃用ScheduledThreadPoolExecutor,转而使用Timer,劝君三思,根据我的实验,当任务执行时间吗,没有其周期长的时候,ScheduledThreadPoolExecutor的间隔比Timer准确多了。
所以,还是使用ScheduledThreadPoolExecutor吧!
可能有的同学会说,这样的定时器太古老了,都不是设定每天定点触发……
胡说,我来给你们写一个:
/**
* 每天晚上8点执行一次
* 每天定时安排任务进行执行
*/
@Test
public void executeEightAtNightPerDay()
{
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
long oneDay = 24 * 60 * 60 * 1000;
long initDelay = getTimeMillis("20:00:00") - System.currentTimeMillis();
initDelay = initDelay > 0 ? initDelay : oneDay + initDelay;
executor.scheduleAtFixedRate(
new Runnable()
{
@Override
public void run()
{
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("This is a echo server. The current time is " +
System.currentTimeMillis() + ".");
}
},
initDelay,
oneDay,
TimeUnit.MILLISECONDS);
}
/**
* 获取指定时间对应的毫秒数
* @param time "HH:mm:ss"
* @return
*/
private static long getTimeMillis(String time)
{
try
{
DateFormat dateFormat = new SimpleDateFormat("yy-MM-dd HH:mm:ss");
DateFormat dayFormat = new SimpleDateFormat("yy-MM-dd");
Date curDate = dateFormat.parse(dayFormat.format(new Date()) + " " + time);
return curDate.getTime();
} catch (ParseException e)
{
e.printStackTrace();
}
return 0;
}
没有困难的工作,只有坚强的打工人!