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

Posted

技术标签:

【中文标题】如何使用 ScheduledExecutorService 每天在特定时间运行特定任务?【英文标题】:How to run certain task every day at a particular time using ScheduledExecutorService? 【发布时间】:2013-12-21 16:16:41 【问题描述】:

我每天早上 5 点尝试执行某项任务。所以我决定为此使用ScheduledExecutorService,但到目前为止,我已经看到了一些示例,这些示例展示了如何每隔几分钟运行一次任务。

而且我找不到任何示例来说明如何在每天早上的特定时间(凌晨 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();
    

有什么办法,考虑到夏令时的事实,我可以使用ScheduledExecutorService 安排每天早上 5 点运行的任务吗?

还有TimerTask 更适合这个还是ScheduledExecutorService

【问题讨论】:

改用 Quartz 之类的东西。 【参考方案1】:

这里假设代码在单个 VM 上运行。如果代码在多个 VM 上运行,则此调度程序代码将运行多次。这必须单独处理。

【讨论】:

您的帖子更多的是评论而不是问题的实际答案。 是的,你是对的,但我无权评论主要答案,所以我只能这样评论。【参考方案2】:

如果你能像这样写,为什么要把情况复杂化? (是的->低内聚,硬编码->但它是一个例子,不幸的是使用命令方式)。有关其他信息,请阅读下面的代码示例;))

package timer.test;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.concurrent.*;

public class TestKitTimerWithExecuterService 

    private static final Logger log = LoggerFactory.getLogger(TestKitTimerWithExecuterService.class);

    private static final ScheduledExecutorService executorService 
= Executors.newSingleThreadScheduledExecutor();// equal to => newScheduledThreadPool(1)/ Executor service with one Thread

    private static ScheduledFuture<?> future; // why? because scheduleAtFixedRate will return you it and you can act how you like ;)



    public static void main(String args[])

        log.info("main thread start");

        Runnable task = () -> log.info("******** Task running ********");

        LocalDateTime now = LocalDateTime.now();

        LocalDateTime whenToStart = LocalDate.now().atTime(20, 11); // hour, minute

        Duration duration = Duration.between(now, whenToStart);

        log.info("WhenToStart : , Now : , Duration/difference in second : ",whenToStart, now, duration.getSeconds());

        future = executorService.scheduleAtFixedRate(task
                , duration.getSeconds()    //  difference in second - when to start a job
                ,2                         // period
                , TimeUnit.SECONDS);

        try 
            TimeUnit.MINUTES.sleep(2);  // DanDig imitation of reality
            cancelExecutor();    // after canceling Executor it will never run your job again
         catch (InterruptedException e) 
            e.printStackTrace();
        
        log.info("main thread end");
    




    public static void cancelExecutor()

        future.cancel(true);
        executorService.shutdown();

        log.info("Executor service goes to shut down");
    


【讨论】:

【参考方案3】:

如果您的服务器在凌晨 4:59 出现故障并在凌晨 5:01 恢复,该怎么办?我认为它只会跳过运行。我会推荐像 Quartz 这样的持久调度器,它将其调度数据存储在某个地方。然后它会看到此运行尚未执行,并将在凌晨 5:01 执行。

【讨论】:

【参考方案4】:

与当前的 java SE 8 版本一样,它具有出色的日期时间 API 和 java.time,这些计算可以更轻松地完成,而不是使用 java.util.Calendarjava.util.Date

使用这个新 API 的date time class's i.e. LocalDateTime 使用ZonedDateTime 类来处理时区特定的计算,包括夏令时问题。你会在这里找到tutorial and example。

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

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 initalDelay = duration.getSeconds();

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);            
scheduler.scheduleAtFixedRate(new MyRunnableTask(),
    initalDelay,
    TimeUnit.DAYS.toSeconds(1),
    TimeUnit.SECONDS);

计算initalDelay 是为了让调度程序延迟TimeUnit.SECONDS 中的执行。对于此用例,单位毫秒及以下的时差问题似乎可以忽略不计。但是您仍然可以使用duration.toMillis()TimeUnit.MILLISECONDS 以毫秒为单位处理调度计算。

还有 TimerTask 更适合这个还是 ScheduledExecutorService?

否: ScheduledExecutorService 似乎比 TimerTask 好。 *** has already an answer for you。

来自@PaddyD,

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

确实如此,@PaddyD 已经给出了解决方法(给他+1),我提供了一个带有ScheduledExecutorService 的 Java8 日期时间 API 的工作示例。 Using daemon thread is dangerous

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 时,在对其调用 awaitTermination 后始终使用 awaitTermination:您的任务总是有可能卡住/死锁,而用户将永远等待。

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

【讨论】:

感谢您的建议,能否请您详细解释一下intDelayInHour 表示我将在早上 5 点运行我的任务? aDate 的用途是什么? 但是如果你在 HH:mm 开始这个任务会在 05:mm 运行而不是早上 5 点?它也没有按照 OP 的要求考虑夏令时。好的,如果您在一小时后立即启动它,或者如果您对 5 到 6 点之间的任何时间感到满意,或者如果您不介意在时钟改变后每年两次在半夜重新启动应用程序我想... 如果您希望它在正确的本地时间运行,您仍然需要每年重新启动两次。 scheduleAtFixedRate 不会削减它,除非您对全年相同的 UTC 时间感到满意。 为什么下面的例子(第二个)触发器执行n次或直到第二个通过?代码不是应该每天触发一次任务吗?【参考方案5】:

您可以使用下面的课程来安排您每天特定时间的任务

package interfaces;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class CronDemo implements Runnable

    public static void main(String[] args) 

        Long delayTime;

        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

        final Long initialDelay = LocalDateTime.now().until(LocalDate.now().plusDays(1).atTime(12, 30), ChronoUnit.MINUTES);

        if (initialDelay > TimeUnit.DAYS.toMinutes(1)) 
            delayTime = LocalDateTime.now().until(LocalDate.now().atTime(12, 30), ChronoUnit.MINUTES);
         else 
            delayTime = initialDelay;
        

        scheduler.scheduleAtFixedRate(new CronDemo(), delayTime, TimeUnit.DAYS.toMinutes(1), TimeUnit.MINUTES);

    

    @Override
    public void run() 
        System.out.println("I am your job executin at:" + new Date());
    

【讨论】:

请不要使用2019年过时的DateTimeUnit【参考方案6】:

以下示例对我有用

public class DemoScheduler 

    public static void main(String[] args) 

        // Create a calendar instance
        Calendar calendar = Calendar.getInstance();

        // Set time of execution. Here, we have to run every day 4:20 PM; so,
        // setting all parameters.
        calendar.set(Calendar.HOUR, 8);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.AM_PM, Calendar.AM);

        Long currentTime = new Date().getTime();

        // Check if current time is greater than our calendar's time. If So,
        // then change date to one day plus. As the time already pass for
        // execution.
        if (calendar.getTime().getTime() < currentTime) 
            calendar.add(Calendar.DATE, 1);
        

        // Calendar is scheduled for future; so, it's time is higher than
        // current time.
        long startScheduler = calendar.getTime().getTime() - currentTime;

        // Setting stop scheduler at 4:21 PM. Over here, we are using current
        // calendar's object; so, date and AM_PM is not needed to set
        calendar.set(Calendar.HOUR, 5);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.AM_PM, Calendar.PM);

        // Calculation stop scheduler
        long stopScheduler = calendar.getTime().getTime() - currentTime;

        // Executor is Runnable. The code which you want to run periodically.
        Runnable task = new Runnable() 

            @Override
            public void run() 

                System.out.println("test");
            
        ;

        // Get an instance of scheduler
        final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        // execute scheduler at fixed time.
        scheduler.scheduleAtFixedRate(task, startScheduler, stopScheduler, MILLISECONDS);
    

参考: https://chynten.wordpress.com/2016/06/03/java-scheduler-to-run-every-day-on-specific-time/

【讨论】:

【参考方案7】:

只是为了补充Victor's answer。

我建议添加检查变量(在他的情况下为长 midnight)是否高于 1440。如果是,我会省略.plusDays(1),否则任务只会在后天运行。

我就是这么干的:

Long time;

final Long tempTime = LocalDateTime.now().until(LocalDate.now().plusDays(1).atTime(7, 0), ChronoUnit.MINUTES);
if (tempTime > 1440) 
    time = LocalDateTime.now().until(LocalDate.now().atTime(7, 0), ChronoUnit.MINUTES);
 else 
    time = tempTime;

【讨论】:

如果你使用truncatedTo()会简化【参考方案8】:

您是否考虑过使用Quartz Scheduler 之类的东西?这个库有一种机制,可以使用类似 cron 的表达式来安排任务在每天设定的时间段内运行(看看 CronScheduleBuilder)。

一些示例代码(未测试):

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();
    

还有一些前期工作,您可能需要重写您的作业执行代码,但它应该能让您更好地控制您希望您的作业如何运行。如果您需要,也可以更轻松地更改时间表。

【讨论】:

【参考方案9】:

您可以使用简单的日期解析,如果一天中的时间在现在之前,让我们明天开始:

  String timeToStart = "12:17:30";
  SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss");
  SimpleDateFormat formatOnlyDay = new SimpleDateFormat("yyyy-MM-dd");
  Date now = new Date();
  Date dateToStart = format.parse(formatOnlyDay.format(now) + " at " + timeToStart);
  long diff = dateToStart.getTime() - now.getTime();
  if (diff < 0) 
    // tomorrow
    Date tomorrow = new Date();
    Calendar c = Calendar.getInstance();
    c.setTime(tomorrow);
    c.add(Calendar.DATE, 1);
    tomorrow = c.getTime();
    dateToStart = format.parse(formatOnlyDay.format(tomorrow) + " at " + timeToStart);
    diff = dateToStart.getTime() - now.getTime();
  

  ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);            
  scheduler.scheduleAtFixedRate(new MyRunnableTask(), TimeUnit.MILLISECONDS.toSeconds(diff) ,
                                  24*60*60, TimeUnit.SECONDS);

【讨论】:

【参考方案10】:

Java8: 我的最佳答案升级版本:

    修复了 Web 应用程序服务器由于线程池空闲线程而不想停止的情况 没有递归 使用您的自定义当地时间运行任务,在我的情况下,它是白俄罗斯、明斯克

/**
 * 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");
        
    


【讨论】:

我有类似的要求。我必须在给定的时间安排一天的任务。计划任务完成后,在给定时间安排第二天的任务。这应该继续下去。我的问题是如何找出计划任务是否完成。一旦我知道计划的任务已经完成,那么只有我可以安排第二天。【参考方案11】:

如果您没有能力使用 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");

注意计时器任务在不建议执行任何 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 路线走下去,您可能需要一个调用shutdownNow 的停止方法,否则当您尝试停止它时,您的应用程序可能会挂起。

【讨论】:

我明白你的意思。 +1,谢谢。但是,最好不使用 Daemon 线程(即new Timer(runThreadName, true))。 @Sage 不用担心。如果您不进行任何 IO,则可以使用守护线程。我写这个的用例只是一个简单的即发即弃的类,用于启动一些线程来执行一些日常的内务处理任务。我想如果您在计时器任务线程中执行数据库读取操作,那么您不应该使用守护程序,并且需要某种停止方法,您必须调用该方法才能使您的应用程序终止。 ***.com/questions/7067578/… @PaddyD 最后一部分,即使用 ScheduledExecutorServe 的部分是否正确???创建匿名类的方式在语法上看起来并不正确。 newSingleThreadExecutor() 也不会有 schedule 方法吗?? 如果有人卡在 Java 6 和 7 上,我建议他们改用大多数 java.time 功能的后向端口。请参阅ThreeTen-Backport 项目。【参考方案12】:

在 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);

【讨论】:

为了便于阅读,我建议使用TimeUnit.DAYS.toMinutes(1) 而不是“幻数”1440。 谢谢,维克多。这样的话,如果我希望它在正确的本地时间运行,是否需要每年重新启动两次? 固定汇率不应该随着当地时间的变化而改变,创建后变成大约汇率。 怎么会在 23:59:59 触发我的任务? @invzbl3 — 你是对的,因为这不考虑每年在夏令时转换时发生的 23/25 小时天数(或任何夏令时转换量)。 【参考方案13】:

我遇到了类似的问题。我不得不使用ScheduledExecutorService 安排一天中应该执行的一堆任务。 这通过从凌晨 3:30 开始的一项任务来解决,该任务相对于他的当前时间安排所有其他任务。并将自己重新安排在第二天凌晨 3:30。

在这种情况下,夏令时不再是问题。

【讨论】:

以上是关于如何使用 ScheduledExecutorService 每天在特定时间运行特定任务?的主要内容,如果未能解决你的问题,请参考以下文章

[精选] Mysql分表与分库如何拆分,如何设计,如何使用

如果加入条件,我该如何解决。如果使用字符串连接,我如何使用

如何使用本机反应创建登录以及如何验证会话

如何在自动布局中使用约束标识符以及如何使用标识符更改约束? [迅速]

如何使用 AngularJS 的 ng-model 创建一个数组以及如何使用 jquery 提交?

如何使用laravel保存所有行数据每个行名或相等