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


90

我想每天早上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之类的东西。
millimoose 2013年

Answers:


113

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

现在作为使用您的用例计划任务的示例示例:

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以毫秒为单位处理调度computaions。

而且TimerTask对此或ScheduledExecutorService更好吗?

否: ScheduledExecutorService似乎比更好TimerTaskStackOverflow已经为您提供了答案

从@PaddyD,

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

的确如此,@ PaddyD已经给出了解决方法(给他+1),我提供了一个Java8日期时间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,请始终awaitTermination在调用后使用shutdown:始终有可能您的任务被卡住/死锁,并且用户将永远等待。

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


感谢您的建议,请您详细说明一下intDelayInHour我将如何在早上5点开始执行任务?
AKIWEB 2013年

aDate的目的是什么?
何塞·安迪亚斯(JoséAndias)2014年

但是,如果您从HH:mm开始,任务将在05:mm而不是凌晨5点运行吗?它还不考虑OP请求的夏时制时间。好吧,如果您在一个小时后立即启动它,或者您对5到6之间的时间感到满意,或者如果您不介意在时钟改变后每年两次在深夜重新启动应用程序,我想...
PaddyD 2014年

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

3
为什么以下示例(第二个)触发执行n次或直到第二个通过?代码不是应该每天触发一次任务吗?
krizajb

25

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

14
为了提高可读性,我建议TimeUnit.DAYS.toMinutes(1)不要使用“幻数”
1440。– philonous

谢谢,维克多。这样,如果我希望它在正确的本地时间运行,则需要每年重新启动两次?
invzbl3

当本地时间更改时,固定汇率不应更改,创建后,固定汇率应与汇率差不多。
维克多

这怎么触发我23:59:59的任务?
krizajb

为了提高可读性,我建议使用1而不是1440或TimeUnit.DAYS.toMinutes(1),然后使用时间单位TimeUnit.DAYS。;-)
凯莉·黛妮

7

如果您无法使用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);
}

我不确定这种行为,但是您可能需要一个stop方法,shutdownNow如果沿着这ScheduledExecutorService条路线调用该方法,否则您尝试停止它时应用程序可能会挂起。


我明白你的意思。+1,谢谢。但是,最好不要使用Daemon线程(即new Timer(runThreadName, true))。
Sage 2014年

@Sage不用担心。如果您不执行任何IO,则守护程序线程很好。我为此编写的用例只是一个简单的即弃类,用于启动一些线程来执行一些日常家务任务。我想如果您正在按OP的请求指示在计时器任务线程中执行数据库读取,那么您不应该使用守护程序,并且需要某种stop方法,您必须调用该方法来终止应用程序。stackoverflow.com/questions/7067578/…–
PaddyD

@PaddyD是最后一部分,即使用ScheduledExecutorSerive的那个正确吗?创建匿名类的方式在语法上看起来并不正确。另外newSingleThreadExecutor()不会有schedule方法吗?
FReze FRancis

6

您是否考虑过使用Quartz Scheduler之类的工具?该库提供了一种机制,该机制可以使用cron like表达式调度任务在每天的指定时间段运行(请参阅参考资料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();
    }
}

前期还有更多工作要做,您可能需要重写作业执行代码,但是它应该使您可以更好地控制作业的运行方式。此外,如果需要,可以更轻松地更改计划。


5

Java8:
我的最高答案版本:

  1. 修复了由于线程池具有空闲线程而导致Web Application Server不想停止的情况
  2. 没有递归
  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");
        }
    }

}

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

2

我有一个类似的问题。我不得不安排一堆应该在一天之内执行的任务ScheduledExecutorService。通过在凌晨3:30开始执行一项任务来安排相对于他的所有其他任务,可以解决此问题当前时间的。并将自己安排在第二天凌晨3:30。

在这种情况下,日光节约不再是问题。


2

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

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

1

只是为了补充Victor的答案

我建议添加检查,以查看变量(在他的情况下为long 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()
Mark Jeronimus

1

以下示例对我有用

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/


1

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

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

1
请不要使用过时的DateTimeUnit在2019年
Mark Jeronimus

0

如果您的服务器在4:59 AM时关闭并在5:01 AM时返回,该怎么办?我认为它只会跳过运行。我建议像Quartz这样的持久性调度程序,它将其调度数据存储在某个地方。然后它将看到尚未执行此运行,并将在上午5:01执行。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.