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

답변:


113

현재 Java SE 8 릴리스와 마찬가지로 java.time이러한 종류의 계산이 포함 된 우수한 날짜 시간 API 를 사용하면 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);

initalDelay의 실행을 지연하기 위해 스케줄러를 물어 계산된다 TimeUnit.SECONDS. 단위 밀리 초 이하의 시차 문제는이 사용 사례에서 무시할 수있는 것으로 보입니다. 그러나 여전히 밀리 초 단위로 스케줄링 계산을 사용 duration.toMillis()하고 TimeUnit.MILLISECONDS처리 할 수 있습니다 .

또한 TimerTask가 이것 또는 ScheduledExecutorService에 더 낫습니까?

NO : ScheduledExecutorService 겉보기보다 나은 TimerTask. StackOverflow는 이미 답을 가지고 있습니다 .

@PaddyD에서

올바른 현지 시간에 실행하려면 1 년에 두 번 다시 시작해야하는 문제가 있습니다. scheduleAtFixedRate는 일년 내내 동일한 UTC 시간에 만족하지 않는 한 그것을 자르지 않을 것입니다.

사실이고 @PaddyD가 이미 해결 방법 (+1)을 제공 했으므로 .NET과 함께 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의 목적은 무엇입니까?
호세 Andias

그러나 HH : mm에서 시작하면 작업이 오전 5 시가 아닌 05 : mm에 실행됩니다. 또한 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

고마워, 빅터. 이런 식으로 올바른 현지 시간에 실행하려면 1 년에 두 번 다시 시작해야합니까?
invzbl3

고정 요금은 현지 시간이 변경 될 때 변경되지 않아야하며 생성 된 후에는 요금에 대한 것입니다.
Victor

23:59:59에 내 작업이 트리거되는 이유는 무엇입니까?
krizajb

가독성을 위해 1440 또는 TimeUnit.DAYS.toMinutes (1) 대신 1을 제안한 다음 시간 단위 TimeUnit.DAYS를 사용합니다. ;-)
Kelly Denehy

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

NB 타이머 작업은 IO를 수행하는 데 권장되지 않는 데몬 스레드에서 실행됩니다. 사용자 스레드를 사용해야하는 경우 타이머를 취소하는 다른 메서드를 추가해야합니다.

를 사용해야하는 ScheduledExecutorService경우 startTimer방법을 다음과 같이 변경하면 됩니다.

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

동작을 잘 모르겠지만 경로를 shutdownNow따라 내려 가면 호출하는 stop 메서드가 필요할 수 있습니다 ScheduledExecutorService. 그렇지 않으면 중지하려고 할 때 응용 프로그램이 중단 될 수 있습니다.


나는 당신의 요지를 얻었습니다. +1하고 감사합니다. 그러나 데몬 스레드 (예 :)를 사용하지 않는 것이 좋습니다 new Timer(runThreadName, true).
Sage

@Sage 걱정 마세요. IO를 수행하지 않는 경우 데몬 스레드는 괜찮습니다. 내가 작성한 사용 사례는 일상적인 관리 작업을 수행하기 위해 일부 스레드를 시작하는 간단한 fire-and-forget 클래스였습니다. OP의 요청에 따라 타이머 작업 스레드에서 데이터베이스 읽기를 수행하는 경우 데몬을 사용하지 않아야하며 응용 프로그램을 종료하려면 호출해야하는 일종의 중지 메서드가 필요합니다. stackoverflow.com/questions/7067578/…
PaddyD 2014

@PaddyD 마지막 부분 즉, ScheduledExecutorSerive를 사용하는 부분이 맞습니까 ??? 익명 클래스가 생성되는 방식이 올바른 구문으로 보이지 않습니다. 또한 newSingleThreadExecutor ()는 일정 방법이 맞지 않습니까 ??
FReeze 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
구식 DateTimeUnit2019 년 에는 사용하지 마십시오
Mark Jeronimus

0

서버가 4:59 AM에 다운되었다가 5:01 AM에 돌아 오면 어떻게 될까요? 나는 그것이 실행을 건너 뛸 것이라고 생각합니다. 일정 데이터를 어딘가에 저장하는 Quartz와 같은 영구 스케줄러를 권장합니다. 그러면이 실행이 아직 수행되지 않았으며 오전 5시 1 분에 수행됩니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.