Вопрос-ответ

How to run certain task every day at a particular time using ScheduledExecutorService?

Как запускать определенную задачу каждый день в определенное время с помощью 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();
}
}

Есть ли какой-нибудь способ, я могу запланировать выполнение задачи каждый день в 5 утра, используя ScheduledExecutorService учитывая также факт перехода на летнее время?

А также TimerTask что лучше для этого или ScheduledExecutorService?

Переведено автоматически
Ответ 1

Как и в нынешней версии java SE 8 с ее отличным API даты и времени, с java.time такие вычисления можно выполнять проще, чем с помощью java.util.Calendar и 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 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 уже есть ответ для вас.

От @PaddyD,


У вас все еще есть проблема, из-за которой вам нужно перезапускать это дважды в год, если вы хотите, чтобы оно запускалось в нужное местное время. scheduleAtFixedRate не сократит ее, если вас не устраивает одно и то же время UTC в течение всего года.


Поскольку это правда, и @PaddyD уже предоставил обходной путь (+ 1 к нему), я привожу рабочий пример с Java8 date time API с ScheduledExecutorService. Использование daemon thread опасно

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 after invoking shutdown для нее: всегда есть вероятность, что ваша задача застряла / зашла в тупик, и пользователь будет ждать вечно.

Предыдущий пример, который я привел с Calender, был просто идеей, о которой я упоминал, я избежал проблем с точным расчетом времени и переходом на летнее время. Обновил решение в соответствии с жалобой @PaddyD

Ответ 2

В 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);
Ответ 3

Если вы не можете позволить себе роскошь использовать 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");

Примечание. задача таймера выполняется в потоке демона, который не рекомендуется для выполнения каких-либо операций ввода-вывода. Если вам нужно использовать пользовательский поток, вам нужно будет добавить другой метод, который отменяет таймер.

Если вам нужно использовать 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 маршруту, в противном случае ваше приложение может зависнуть, когда вы попытаетесь его остановить.

Ответ 4

Java8:
Моя обновленная версия из верхнего ответа:


  1. Исправлена ситуация, когда сервер веб-приложений не хотел останавливаться из-за threadpool с незанятым потоком

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

}
java