Bagaimana cara menjalankan tugas tertentu setiap hari pada waktu tertentu menggunakan ScheduledExecutorService?


90

Saya mencoba menjalankan tugas tertentu setiap hari pada jam 5 pagi. Jadi saya memutuskan untuk menggunakan ScheduledExecutorServiceini tetapi sejauh ini saya telah melihat contoh yang menunjukkan bagaimana menjalankan tugas setiap beberapa menit.

Dan saya tidak dapat menemukan contoh apa pun yang menunjukkan bagaimana menjalankan tugas setiap hari pada waktu tertentu (5 pagi) di pagi hari dan juga mempertimbangkan fakta waktu musim panas juga -

Di bawah ini adalah kode saya yang akan berjalan setiap 15 menit -

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

Adakah cara, saya dapat menjadwalkan tugas untuk dijalankan setiap hari jam 5 pagi dengan ScheduledExecutorServicemempertimbangkan fakta waktu musim panas juga?

Dan juga TimerTasklebih baik untuk ini atau ScheduledExecutorService?


Gunakan sesuatu seperti Quartz.
millimoose

Jawaban:


113

Seperti rilis java SE 8 saat java.timeini dengan API waktu tanggal yang sangat baik dengan perhitungan semacam ini dapat dilakukan dengan lebih mudah daripada menggunakan java.util.Calendardan java.util.Date.

Sekarang sebagai contoh contoh untuk menjadwalkan tugas dengan kasus penggunaan Anda:

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

The initalDelaydihitung untuk meminta scheduler untuk menunda eksekusi dalam TimeUnit.SECONDS. Masalah perbedaan waktu dengan satuan milidetik dan di bawahnya tampaknya dapat diabaikan untuk kasus penggunaan ini. Tetapi Anda masih dapat menggunakan duration.toMillis()dan TimeUnit.MILLISECONDSuntuk menangani perhitungan penjadwalan dalam milidetik.

Dan juga TimerTask lebih baik untuk ini atau ScheduledExecutorService?

TIDAK: ScheduledExecutorService tampaknya lebih baik dari TimerTask. StackOverflow sudah menjadi jawaban untuk Anda .

Dari @PaddyD,

Anda masih memiliki masalah di mana Anda perlu memulai ulang ini dua kali setahun jika Anda ingin menjalankannya pada waktu lokal yang tepat. scheduleAtFixedRate tidak akan memotongnya kecuali Anda senang dengan waktu UTC yang sama sepanjang tahun.

Karena memang benar dan @PaddyD telah memberikan solusi (+1 untuk dia), saya memberikan contoh kerja dengan API tanggal waktu Java8 ScheduledExecutorService. Menggunakan utas daemon berbahaya

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

catatan:

  • MyTaskadalah antarmuka dengan fungsi execute.
  • Saat berhenti ScheduledExecutorService, Selalu gunakan awaitTerminationsetelah menjalankannya shutdown: Selalu ada kemungkinan tugas Anda macet / macet dan pengguna akan menunggu selamanya.

Contoh sebelumnya yang saya berikan dengan Kalender hanyalah sebuah ide yang saya sebutkan, saya menghindari perhitungan waktu yang tepat dan masalah penghematan Daylight. Memperbarui solusi sesuai keluhan @PaddyD


Terima kasih atas sarannya, dapatkah Anda menjelaskan kepada saya secara rinci bagaimana intDelayInHoursaya akan menjalankan tugas saya pada jam 5 pagi?
AKIWEB

Apa tujuan aDate?
José Andias

Tetapi jika Anda memulai ini di HH: mm tugas akan dijalankan pada 05: mm sebagai lawan 5 pagi? Itu juga tidak memperhitungkan waktu musim panas seperti yang diminta OP. Oke jika Anda memulainya segera setelah satu jam, atau jika Anda senang dengan waktu antara 5 dan 6, atau jika Anda tidak keberatan memulai ulang aplikasi di tengah malam dua kali setahun setelah jam berubah, saya kira ...
PaddyD

2
Anda masih memiliki masalah di mana Anda perlu memulai ulang ini dua kali setahun jika Anda ingin menjalankannya pada waktu lokal yang tepat. scheduleAtFixedRatetidak akan memotongnya kecuali Anda senang dengan waktu UTC yang sama sepanjang tahun.
PaddyD

3
Mengapa contoh berikut (detik) memicu dieksekusi sebanyak n kali atau hingga detik berlalu? Bukankah kodenya seharusnya memicu tugas sekali setiap hari?
krizajb

25

Di Jawa 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
Untuk keterbacaan saya akan menyarankan TimeUnit.DAYS.toMinutes(1)alih-alih "angka ajaib" 1440.
filonos

Terima kasih, Victor. Dengan cara ini perlu memulai ulang dua kali setahun jika saya ingin berjalan pada waktu lokal yang tepat?
invzbl3

tarif tetap tidak boleh berubah ketika waktu lokal berubah, setelah dibuat, itu menjadi tentang tarif.
Victor

Kenapa ini memicu tugas saya pada pukul 23:59:59?
krizajb

Untuk keterbacaan saya akan menyarankan 1 daripada 1440 atau TimeUnit.DAYS.toMinutes (1), dan kemudian menggunakan unit waktu TimeUnit.DAYS. ;-)
Kelly Denehy

7

Jika Anda tidak memiliki kemewahan untuk dapat menggunakan Java 8, berikut ini akan melakukan apa yang Anda butuhkan:

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

Itu tidak memerlukan lib eksternal, dan akan memperhitungkan penghematan siang hari. Cukup lewati saat Anda ingin menjalankan tugas sebagai Calendarobjek, dan tugas sebagai Runnable. Sebagai contoh:

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 tugas pengatur waktu berjalan di utas Daemon yang tidak disarankan untuk melakukan IO apa pun. Jika Anda perlu menggunakan utas Pengguna, Anda perlu menambahkan metode lain yang membatalkan pengatur waktu.

Jika Anda harus menggunakan a ScheduledExecutorService, cukup ubah startTimermetode berikut ini:

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

Saya tidak yakin dengan perilakunya tetapi Anda mungkin memerlukan metode berhenti yang memanggil shutdownNowjika Anda melalui ScheduledExecutorServicerute, jika tidak, aplikasi Anda mungkin macet ketika Anda mencoba menghentikannya.


Saya mengerti maksud Anda. +1 dan terima kasih. Namun, lebih baik jika kita tidak menggunakan utas Daemon (mis new Timer(runThreadName, true).).
Sage

@Sage jangan khawatir. Untaian daemon baik-baik saja jika Anda tidak melakukan IO apa pun. Kasus penggunaan yang saya tulis ini hanyalah kelas api-dan-lupa sederhana untuk memulai beberapa utas untuk melakukan beberapa tugas rumah tangga harian. Saya kira jika Anda melakukan pembacaan database di utas tugas pengatur waktu seperti yang mungkin ditunjukkan oleh permintaan OP, maka Anda tidak boleh menggunakan Daemon dan akan memerlukan semacam metode berhenti yang harus Anda panggil untuk mengaktifkan aplikasi Anda untuk dihentikan. stackoverflow.com/questions/7067578/…
PaddyD

@PaddyD Apakah bagian terakhir yaitu yang menggunakan ScheduledExecutorSerive benar ??? Cara kelas anonim dibuat tampaknya tidak memiliki sintaks yang benar. Juga newSingleThreadExecutor () tidak akan memiliki metode jadwal kan ??
FRANCIS FRancis

6

Pernahkah Anda mempertimbangkan untuk menggunakan sesuatu seperti Penjadwal Kuarsa ? Pustaka ini memiliki mekanisme untuk menjadwalkan tugas agar berjalan pada periode waktu tertentu setiap hari menggunakan ekspresi seperti cron (lihatlah CronScheduleBuilder).

Beberapa kode contoh (tidak diuji):

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

Ada sedikit lebih banyak pekerjaan di muka, dan Anda mungkin perlu menulis ulang kode eksekusi pekerjaan Anda, tetapi itu akan memberi Anda lebih banyak kendali atas bagaimana Anda ingin pekerjaan Anda dijalankan. Juga akan lebih mudah untuk mengubah jadwal jika Anda perlu.


5

Java8:
Versi peningkatan saya dari jawaban teratas:

  1. Memperbaiki situasi ketika Server Aplikasi Web tidak ingin berhenti, karena threadpool dengan thread yang tidak aktif
  2. Tanpa rekursi
  3. Jalankan tugas dengan waktu lokal pilihan Anda, dalam kasus saya, itu Belarusia, Minsk


/**
 * 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
Saya memiliki persyaratan serupa. Saya harus menjadwalkan tugas untuk hari pada waktu tertentu. Setelah tugas terjadwal selesai, jadwalkan tugas untuk hari berikutnya pada waktu tertentu. Ini harus terus berjalan. Pertanyaan saya adalah bagaimana mengetahui tugas terjadwal telah selesai atau belum. Setelah saya tahu tugas terjadwal telah selesai maka hanya saya yang dapat menjadwalkan untuk hari berikutnya.
amitwdh

2

Saya punya masalah serupa. Saya harus menjadwalkan banyak tugas yang harus dijalankan selama sehari menggunakan ScheduledExecutorService. Ini diselesaikan dengan satu tugas mulai pukul 3:30 pagi yang menjadwalkan semua tugas lain secara relatif ke waktu saat ini . Dan menjadwalkan ulang dirinya sendiri untuk hari berikutnya pada jam 3:30 pagi.

Dengan skenario ini penghematan siang hari tidak menjadi masalah lagi.


2

Anda dapat menggunakan penguraian tanggal sederhana, jika waktu sebelum sekarang, mari kita mulai besok:

  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

Hanya untuk menambahkan jawaban Victor .

Saya akan merekomendasikan untuk menambahkan tanda centang untuk melihat, apakah variabel (dalam kasusnya panjang midnight) lebih tinggi dari 1440. Jika ya, saya akan menghilangkan .plusDays(1), jika tidak, tugas hanya akan berjalan lusa.

Saya melakukannya seperti ini:

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

Ini menyederhanakan jika Anda menggunakantruncatedTo()
Mark Jeronimus

1

Contoh berikut berhasil untuk saya

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

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


1

Anda dapat menggunakan kelas di bawah ini untuk menjadwalkan tugas Anda setiap hari pada waktu tertentu

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
Tolong jangan gunakan yang ketinggalan zaman Datedan TimeUnitdi 2019
Mark Jeronimus

0

Bagaimana jika server Anda mati pada pukul 4:59 dan kembali pada pukul 5:01? Saya pikir itu hanya akan melewatkan lari. Saya akan merekomendasikan penjadwal persisten seperti Quartz, yang akan menyimpan data jadwalnya di suatu tempat. Kemudian akan terlihat bahwa proses ini belum dilakukan dan akan melakukannya pada 5:01.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.