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

AKI*_*WEB 65 java background-thread timertask scheduledexecutorservice

我每天凌晨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();
    }
}
Run Code Online (Sandbox Code Playgroud)

有没有办法,我可以安排一项任务,每天早上5点运行,同时ScheduledExecutorService考虑夏令时的事实?

TimerTask就是这个或更好ScheduledExecutorService

Sag*_*age 84

与目前的Java SE 8版本一样,它具有出色的日期时间API,java.time这种计算可以更轻松地完成,而不是使用 java.util.Calendarjava.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);
Run Code Online (Sandbox Code Playgroud)

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

而TimerTask对于这个还是ScheduledExecutorService更好?

不: ScheduledExecutorService看似好过TimerTask.StackOverflow已经为您解答了.

来自@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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

注意:

  • MyTask是一个功能的接口execute.
  • 在停止时ScheduledExecutorService,总是awaitTermination在调用shutdown它之后使用:总是有可能你的任务卡住/死锁,用户会永远等待.

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

  • 为什么以下示例(第二个)触发器执行 n 次或直到第二个通过?代码不是应该每天触发一次任务吗? (4认同)
  • 如果您希望在当地正确的时间运行,您仍然需要每年重启两次.`scheduleAtFixedRate`除非你对全年的UTC时间感到满意,否则不会削减它. (2认同)

Vic*_*tor 20

在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);
Run Code Online (Sandbox Code Playgroud)

  • 为了便于阅读,我建议使用`TimeUnit.DAYS.toMinutes(1)`而不是"幻数"1440. (12认同)
  • @invzbl3 - 你是对的,因为这并没有考虑到每年夏令时切换时发生的 23/25 小时天(或夏令时偏移可能的量)。 (2认同)
  • 老实说,这应该是公认的答案,与其他答案相比,它更加清晰和简单 (2认同)

Pad*_*dyD 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();
   }
}
Run Code Online (Sandbox Code Playgroud)

它不需要任何外部库,并将占用夏令时.只需传递您想要将任务作为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");
Run Code Online (Sandbox Code Playgroud)

注意,计时器任务在守护程序线程中运行,不建议执行任何IO.如果需要使用User线程,则需要添加另一个取消计时器的方法.

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

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

我不确定该行为,但您可能需要一个停止方法,shutdownNow如果您沿着ScheduledExecutorService路线走,则会调用,否则当您尝试停止时,您的应用程序可能会挂起.


lmi*_*ika 5

您是否考虑过使用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();
    }
}
Run Code Online (Sandbox Code Playgroud)

预先有一些工作,你可能需要重写你的工作执行代码,但它应该可以让你更好地控制你的工作运行方式.如果需要,也可以更容易地更改计划.


Ale*_*nka 5

Java8:
我的升级版本从顶部答案:

  1. 修复Web应用程序服务器不想停止的情况,因为线程池具有空闲线程
  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");
        }
    }

}
Run Code Online (Sandbox Code Playgroud)