如何使用ScheduledExecutorService在特定的时间每天运行某些任务?

我正在尝试在凌晨5点每天运行一些任务。 所以我决定使用ScheduledExecutorService ,但是到目前为止,我已经看到了一些例子,说明如何每几分钟运行一次任务。

而且我无法find任何例子,说明如何在早上的特定时间(凌晨5点)每天运行一项任务,同时也考虑夏令时的事实 –

下面是我的代码,每15分钟运行一次 –

 public class ScheduledTaskExample { private final ScheduledExecutorService scheduler = Executors .newScheduledThreadPool(1); public void startScheduleTask() { /** * not using the taskHandle returned here, but it can be used to cancel * the task, or check if it's done (for recurring tasks, that's not * going to be very useful) */ final ScheduledFuture<?> taskHandle = scheduler.scheduleAtFixedRate( new Runnable() { public void run() { try { getDataFromDatabase(); }catch(Exception ex) { ex.printStackTrace(); //or loggger would be better } } }, 0, 15, TimeUnit.MINUTES); } private void getDataFromDatabase() { System.out.println("getting data..."); } public static void main(String[] args) { ScheduledTaskExample ste = new ScheduledTaskExample(); ste.startScheduleTask(); } } 

有没有办法,我可以安排一个任务,每天凌晨5点使用ScheduledExecutorService考虑夏令时的事实每天运行?

而且TimerTask更适合这个或ScheduledExecutorService

与现在的java SE 8版本一样,它具有java.time的优秀date时间API,这些计算可以更容易地完成,而不是使用java.util.Calendarjava.util.Date

  • 使用这个新的API的date时间类的LocalDateTime
  • 使用ZonedDateTime类来处理时区特定的计算,包括夏令时问题。 你会在这里find教程和例子 。

现在作为使用您的用例来安排任务的示例:

  LocalDateTime localNow = LocalDateTime.now(); ZoneId currentZone = ZoneId.of("America/Los_Angeles"); ZonedDateTime zonedNow = ZonedDateTime.of(localNow, currentZone); ZonedDateTime zonedNext5 ; zonedNext5 = zonedNow.withHour(5).withMinute(0).withSecond(0); if(zonedNow.compareTo(zonedNext5) > 0) zonedNext5 = zonedNext5.plusDays(1); Duration duration = Duration.between(zonedNow, zonedNext5); long initalDelay = duration.getSeconds(); ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); scheduler.scheduleAtFixedRate(new MyRunnableTask(), initalDelay, 24*60*60, TimeUnit.SECONDS); 

计算initalDelay是要求调度程序在TimeUnit.SECONDS延迟执行。 单位毫秒及以下的时间差异问题似乎可以忽略不计。 但是仍然可以使用duration.toMillis()TimeUnit.MILLISECONDS来处理调度计算(毫秒)。

而且TimerTask更适合这个或ScheduledExecutorService?

NO: ScheduledExecutorService看起来比TimerTask 。 StackOverflow已经为你解答了 。

从@PaddyD,

您仍然有这个问题,如果您希望在当地正确运行,您需要每年重新启动两次。 scheduleAtFixedRate不会削减它,除非你对全年的UTC时间感到满意。

由于这是真的,@ PaddyD已经给出了一个解决方法(+1给他),我提供了一个Java8的date时间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(){ @Override 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 :任务总是有可能被阻塞/死锁,用户将永远等待。

之前我用Calender给出的例子只是我提到的一个想法 ,我避免了精确的时间计算和夏令时问题。 根据@PaddyD的抱怨更新解决scheme

如果你没有能够使用Java 8的豪华,下面将做你所需要的:

 public class DailyRunnerDaemon { private final Runnable dailyTask; private final int hour; private final int minute; private final int second; private final String runThreadName; public DailyRunnerDaemon(Calendar timeOfDay, Runnable dailyTask, String runThreadName) { this.dailyTask = dailyTask; this.hour = timeOfDay.get(Calendar.HOUR_OF_DAY); this.minute = timeOfDay.get(Calendar.MINUTE); this.second = timeOfDay.get(Calendar.SECOND); this.runThreadName = runThreadName; } public void start() { startTimer(); } private void startTimer(); { new Timer(runThreadName, true).schedule(new TimerTask() { @Override public void run() { dailyTask.run(); startTimer(); } }, getNextRunTime()); } private Date getNextRunTime() { Calendar startTime = Calendar.getInstance(); Calendar now = Calendar.getInstance(); startTime.set(Calendar.HOUR_OF_DAY, hour); startTime.set(Calendar.MINUTE, minute); startTime.set(Calendar.SECOND, second); startTime.set(Calendar.MILLISECOND, 0); if(startTime.before(now) || startTime.equals(now)) { startTime.add(Calendar.DATE, 1); } return startTime.getTime(); } } 

它不需要任何外部库,并将占夏时制。 只需将您希望运行任务的时间作为Calendar对象传递,并将任务作为Runnable传递。 例如:

 Calendar timeOfDay = Calendar.getInstance(); timeOfDay.set(Calendar.HOUR_OF_DAY, 5); timeOfDay.set(Calendar.MINUTE, 0); timeOfDay.set(Calendar.SECOND, 0); new DailyRunnerDaemon(timeOfDay, new Runnable() { @Override public void run() { try { // call whatever your daily task is here doHousekeeping(); } catch(Exception e) { logger.error("An error occurred performing daily housekeeping", e); } } }, "daily-housekeeping"); 

NB计时器任务运行在一个守护程序线程,不build议做任何IO。 如果您需要使用用户线程,则需要添加另一个取消定时器的方法。

如果您必须使用ScheduledExecutorService ,只需将startTimer方法更改为以下内容:

 private void startTimer() { Executors.newSingleThreadExecutor().schedule(new Runnable() { Thread.currentThread().setName(runThreadName); dailyTask.run(); startTimer(); }, getNextRunTime().getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS); } 

我不确定这种行为,但是如果您沿着ScheduledExecutorService路线行进,则可能需要一个stopNow方法来调用shutdownNow ,否则当您试图阻止它时,您的应用程序可能会挂起。

你有没有考虑使用类似Quartz Scheduler的东西? 这个库有一个调度任务的机制,每天使用一个类似cron的expression式来运行(看看CronScheduleBuilder )。

一些示例代码(未testing):

 public class GetDatabaseJob implements InterruptableJob { public void execute(JobExecutionContext arg0) throws JobExecutionException { getFromDatabase(); } } public class Example { public static void main(String[] args) { JobDetails job = JobBuilder.newJob(GetDatabaseJob.class); // Schedule to run at 5 AM every day ScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0 0 5 * * ?); Trigger trigger = TriggerBuilder.newTrigger(). withSchedule(scheduleBuilder).build(); Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); scheduler.scheduleJob(job, trigger); scheduler.start(); } } 

事先做好了一些工作,你可能需要重写你的工作执行代码,但是它应该让你更好地控制你想要工作的方式。 如果需要,也可以更容易地更改时间表。

在Java 8中:

 scheduler = Executors.newScheduledThreadPool(1); //Change here for the hour you want ----------------------------------.at() Long midnight=LocalDateTime.now().until(LocalDate.now().plusDays(1).atStartOfDay(), ChronoUnit.MINUTES); scheduler.scheduleAtFixedRate(this, midnight, 1440, TimeUnit.MINUTES); 

Java8:
我的升级版顶级回答:

  1. 固定的情况,当Web应用程序服务器不想停止,因为threadpool与空闲线程
  2. 没有recursion
  3. 用你自定义的当地时间运行任务,在我的情况下,它是白俄罗斯,明斯克

 /** * Execute {@link AppWork} once per day. * <p> * Created by aalexeenka on 29.12.2016. */ public class OncePerDayAppWorkExecutor { private static final Logger LOG = AppLoggerFactory.getScheduleLog(OncePerDayAppWorkExecutor.class); private ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); private final String name; private final AppWork appWork; private final int targetHour; private final int targetMin; private final int targetSec; private volatile boolean isBusy = false; private volatile ScheduledFuture<?> scheduledTask = null; private AtomicInteger completedTasks = new AtomicInteger(0); public OncePerDayAppWorkExecutor( String name, AppWork appWork, int targetHour, int targetMin, int targetSec ) { this.name = "Executor [" + name + "]"; this.appWork = appWork; this.targetHour = targetHour; this.targetMin = targetMin; this.targetSec = targetSec; } public void start() { scheduleNextTask(doTaskWork()); } private Runnable doTaskWork() { return () -> { LOG.info(name + " [" + completedTasks.get() + "] start: " + minskDateTime()); try { isBusy = true; appWork.doWork(); LOG.info(name + " finish work in " + minskDateTime()); } catch (Exception ex) { LOG.error(name + " throw exception in " + minskDateTime(), ex); } finally { isBusy = false; } scheduleNextTask(doTaskWork()); LOG.info(name + " [" + completedTasks.get() + "] finish: " + minskDateTime()); LOG.info(name + " completed tasks: " + completedTasks.incrementAndGet()); }; } private void scheduleNextTask(Runnable task) { LOG.info(name + " make schedule in " + minskDateTime()); long delay = computeNextDelay(targetHour, targetMin, targetSec); LOG.info(name + " has delay in " + delay); scheduledTask = executorService.schedule(task, delay, TimeUnit.SECONDS); } private static long computeNextDelay(int targetHour, int targetMin, int targetSec) { ZonedDateTime zonedNow = minskDateTime(); ZonedDateTime zonedNextTarget = zonedNow.withHour(targetHour).withMinute(targetMin).withSecond(targetSec).withNano(0); if (zonedNow.compareTo(zonedNextTarget) > 0) { zonedNextTarget = zonedNextTarget.plusDays(1); } Duration duration = Duration.between(zonedNow, zonedNextTarget); return duration.getSeconds(); } public static ZonedDateTime minskDateTime() { return ZonedDateTime.now(ZoneId.of("Europe/Minsk")); } public void stop() { LOG.info(name + " is stopping."); if (scheduledTask != null) { scheduledTask.cancel(false); } executorService.shutdown(); LOG.info(name + " stopped."); try { LOG.info(name + " awaitTermination, start: isBusy [ " + isBusy + "]"); // wait one minute to termination if busy if (isBusy) { executorService.awaitTermination(1, TimeUnit.MINUTES); } } catch (InterruptedException ex) { LOG.error(name + " awaitTermination exception", ex); } finally { LOG.info(name + " awaitTermination, finish"); } } } 

我有一个类似的问题。 我不得不安排使用ScheduledExecutorService在一天内执行的一系列任务。 这是通过从凌晨3点半开始的一个任务来解决所有其他任务相对于他当前的时间 。 并在凌晨3:30重新安排第二天。

有了这个场景,夏令时就不再是问题了。