在Java中的ManagedSchedulerExecutorService类
在Java中的ManagedSchedulerExecutorService类
我想每天早上5点运行一个特定的任务。因此,我决定使用ScheduledExecutorService
,但到目前为止,我只看到了每隔几分钟运行任务的示例。
我找不到任何示例显示如何在每天的特定时间(早上5点)运行任务,并考虑到夏令时的因素。
下面是我的代码,它将每隔15分钟运行一次:
public class ScheduledTaskExample { private final ScheduledExecutorService scheduler = Executors .newScheduledThreadPool(1); public void startScheduleTask() { /** * 不使用此处返回的taskHandle,但它可以用于取消任务,或者检查任务是否完成(对于重复任务,这将没有多大用处) */ final ScheduledFuture> taskHandle = scheduler.scheduleAtFixedRate( new Runnable() { public void run() { try { getDataFromDatabase(); }catch(Exception ex) { ex.printStackTrace(); //或者最好使用日志记录器 } } }, 0, 15, TimeUnit.MINUTES); } private void getDataFromDatabase() { System.out.println("获取数据..."); } public static void main(String[] args) { ScheduledTaskExample ste = new ScheduledTaskExample(); ste.startScheduleTask(); } }
是否有办法使用ScheduledExecutorService
在每天早上5点运行任务,并考虑到夏令时的因素?
此外,TimerTask
对于这个任务来说更好还是ScheduledExecutorService
更好?
在Java SE 8版本中,引入了新的日期时间API java.time
,使得进行这类计算变得更加容易,而不再需要使用java.util.Calendar
和java.util.Date
。
现在,以一个示例来展示如何使用新的日期时间API调度一个任务:
ZonedDateTime now = ZonedDateTime.now(ZoneId.of("America/Los_Angeles")); ZonedDateTime nextRun = now.withHour(5).withMinute(0).withSecond(0); if(now.compareTo(nextRun) > 0) nextRun = nextRun.plusDays(1); Duration duration = Duration.between(now, nextRun); long initialDelay = duration.getSeconds(); ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); scheduler.scheduleAtFixedRate(new MyRunnableTask(), initialDelay, TimeUnit.DAYS.toSeconds(1), TimeUnit.SECONDS);
initialDelay
的计算通过TimeUnit.SECONDS
来延迟执行。对于毫秒及以下单位的时间差异,在这种情况下似乎可以忽略不计。但是,你仍然可以使用duration.toMillis()
和TimeUnit.MILLISECONDS
来处理以毫秒为单位的调度计算。
那么TimerTask比ScheduledExecutorService更好吗?
不是,ScheduledExecutorService
似乎比TimerTask
更好。在StackOverflow上已经有了答案。
从以下引用中可知,使用scheduleAtFixedRate
存在两次年度重新启动的问题,如果你希望任务以正确的本地时间运行,则无法满足要求。
由于这是真实的,并且已经给出了解决方法(+1给他),我将提供一个使用Java 8日期时间API和ScheduledExecutorService
的工作示例。为了避免使用守护线程,使用守护线程是危险的。
class MyTaskExecutor { ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); MyTask myTask; volatile boolean isStopIssued; public MyTaskExecutor(MyTask myTask$) { myTask = myTask$; } public void startExecutionAt(int targetHour, int targetMin, int targetSec) { Runnable taskWrapper = new Runnable(){ public void run() { myTask.execute(); startExecutionAt(targetHour, targetMin, targetSec); } }; long delay = computeNextDelay(targetHour, targetMin, targetSec); executorService.schedule(taskWrapper, delay, TimeUnit.SECONDS); } private long computeNextDelay(int targetHour, int targetMin, int targetSec) { LocalDateTime localNow = LocalDateTime.now(); ZoneId currentZone = ZoneId.systemDefault(); ZonedDateTime zonedNow = ZonedDateTime.of(localNow, currentZone); ZonedDateTime zonedNextTarget = zonedNow.withHour(targetHour).withMinute(targetMin).withSecond(targetSec); if(zonedNow.compareTo(zonedNextTarget) > 0) zonedNextTarget = zonedNextTarget.plusDays(1); Duration duration = Duration.between(zonedNow, zonedNextTarget); return duration.getSeconds(); } public void stop() { executorService.shutdown(); try { executorService.awaitTermination(1, TimeUnit.DAYS); } catch (InterruptedException ex) { Logger.getLogger(MyTaskExecutor.class.getName()).log(Level.SEVERE, null, ex); } } }
注意:
MyTask
是一个具有execute
函数的接口。- 在停止
ScheduledExecutorService
时,始终在调用shutdown
之后使用awaitTermination
:因为任务可能陷入死锁,用户可能会无限等待。
之前我给出的使用Calendar的示例只是一个想法,我已经提到了避免了确切的时间计算和夏令时问题。根据之前的问题,我更新了解决方案。
感谢提供建议,你能详细解释一下intDelayInHour
的含义吗?这样我就可以在早上5点运行我的任务了。
aDate
的目的是什么?
但是,如果你在HH:mm时启动它,任务将在05:mm运行,而不是在早上5点运行。它也没有解决夏令时的问题,正如OP所要求的那样。如果你立即在整点后启动它,或者如果你满意在5点到6点之间的任何时间,或者如果你不介意在时钟变化后的两天中午夜重启应用程序,那么可以使用这种方法...
你仍然需要每年两次重新启动,如果你希望任务在正确的本地时间运行,scheduleAtFixedRate
不能满足这个要求。
为什么下面的示例(第二个)会触发n次执行或直到时间截止?代码不是应该每天只触发一次任务吗?
也许这是一个不同的问题,我们能使用JobScheduler实现相同的结果吗?
如果计算机休眠,这个示例解决方案似乎不起作用。初始延迟会被冻结,直到计算机唤醒,然后任务将在错误的时间运行。
如果你尝试在午夜之后(或者在午夜)运行一个运行非常快的任务,这个示例解决方案仍然无法正常工作。它有至少两个问题 - 它可能在午夜之前触发,因为计划执行器不是精确的,当触发时,它会一直运行,通常是999次 - 每毫秒运行一次,直到下一毫秒发生。仍然在寻找合适的解决方案...
最好的方法是运行一个快速(不准确的)定时器(我选择每4秒)并在当前时间超过nextRun
时触发事件,然后更新nextRun
以进行下一次调用。类似于这样:private void scheduleNext(){LocalDateTime now=LocalDateTime.now();nextRun=now.truncatedTo(ChronoUnit.DAYS).plusHours(5);if(nextRun.compareTo(now)<=0)nextRun=nextRun.plus(1,ChronoUnit.DAYS);}
在这段代码中,使用了Executors.newScheduledThreadPool(1)
创建了一个只有一个线程的线程池scheduler
。然后通过scheduleAtFixedRate()
方法来设置定时任务的执行时间和间隔时间。
然而,这段代码存在一些问题。首先,使用LocalDateTime.now()
获取当前时间,在存在夏令时(Daylight Savings Time, DST)等时间调整的情况下,会导致时间不准确。因为LocalDateTime
类没有考虑时区或UTC偏移量的上下文。
其次,使用了一个"魔法数字"1440来表示一天的分钟数,不够直观和可读。建议使用TimeUnit.DAYS.toMinutes(1)
来代替。
此外,代码中创建的线程池只有一个线程,这在某些情况下可能会导致问题,比如在存在夏令时转换时,可能需要重启两次才能运行在正确的本地时间。
为了解决这些问题,可以使用ZonedDateTime
类来获取带有时区信息的当前时间,并使用Duration
类来计算时间间隔。同时,可以考虑创建一个具有多个线程的线程池,以便能够并行执行多个任务。
下面是修改后的代码示例:
scheduler = Executors.newScheduledThreadPool(2); ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); ZonedDateTime midnight = now.with(LocalTime.MIDNIGHT).plusDays(1); Duration initialDelay = Duration.between(now, midnight); Duration interval = Duration.ofDays(1); scheduler.scheduleAtFixedRate(this, initialDelay.getSeconds(), interval.getSeconds(), TimeUnit.SECONDS);
通过使用ZonedDateTime
和Duration
类,我们可以更准确地获取当前时间和计算时间间隔。同时,通过使用具有多个线程的线程池,可以并行执行多个任务,提高效率。
总之,这段代码的问题在于使用了LocalDateTime
类来获取当前时间,没有考虑时区和夏令时调整,同时线程池只有一个线程。通过使用ZonedDateTime
和Duration
类来解决时区和时间间隔的问题,以及创建一个具有多个线程的线程池来提高效率。
问题的出现原因:
该问题的出现是因为在Java 8之前的版本中,没有提供类似于Java 8中的ScheduledExecutorService接口来实现定时任务的执行。
解决方法:
为了解决这个问题,可以使用上述给出的代码。代码中定义了一个DailyRunnerDaemon类,通过传入Calendar对象和Runnable对象来实现定时任务的执行。代码中使用了Timer类来调度任务的执行,并且通过递归调用startTimer方法实现了定时执行。
如果需要使用ScheduledExecutorService接口来实现定时任务的执行,可以将startTimer方法修改如下:
private void startTimer() { Executors.newSingleThreadExecutor().schedule(new Runnable() { public void run() { Thread.currentThread().setName(runThreadName); dailyTask.run(); startTimer(); } }, getNextRunTime().getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS); }
需要注意的是,使用ScheduledExecutorService接口需要添加相应的依赖库,并且在代码中可能需要添加停止方法来停止定时任务的执行,以防止应用程序无法终止。
通过上述方法,可以在Java 8之前的版本中实现定时任务的执行。通过使用Timer类或者ScheduledExecutorService接口,可以实现定时执行任务的功能,从而满足特定的业务需求。