如何使用 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

173761 次浏览

As with the present java SE 8 release with it's excellent date time API with java.time these kind of calculation can be done more easily instead of using java.util.Calendar and java.util.Date.

Now as a sample example for scheduling a task with your use case:

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

The initialDelay is computed to ask the scheduler to delay the execution in TimeUnit.SECONDS. Time difference issues with unit milliseconds and below seems to be negligible for this use case. But you can still make use of duration.toMillis() and TimeUnit.MILLISECONDS for handling the scheduling computaions in milliseconds.

And also TimerTask is better for this or ScheduledExecutorService?

NO: ScheduledExecutorService seemingly better than TimerTask. StackOverflow has already an answer for you.

From @PaddyD,

You still have the issue whereby you need to restart this twice a year if you want it to run at the right local time. scheduleAtFixedRate won't cut it unless you are happy with the same UTC time all year.

As it is true and @PaddyD already has given a workaround(+1 to him), I am providing a working example with Java8 date time API with ScheduledExecutorService. Using daemon thread is dangerous

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

Note:

  • MyTask is an interface with function execute.
  • While stopping ScheduledExecutorService, Always use awaitTermination after invoking shutdown on it: There's always a likelihood your task is stuck / deadlocking and the user would wait forever.

The previous example I gave with Calender was just an idea which I did mention, I avoided exact time calculation and Daylight saving issues. Updated the solution on per the complain of @PaddyD

Have you considered using something like Quartz Scheduler? This library has a mechanism for scheduling tasks to run at a set period of time every day using a cron like expression (take a look at CronScheduleBuilder).

Some example code (not tested):

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

There's a bit more work upfront, and you may need to rewrite your job execution code, but it should give you more control over how you want you job to run. Also it would be easier to change the schedule should you need to.

I had a similar problem. I had to schedule bunch of tasks that should be executed during a day using ScheduledExecutorService. This was solved by one task starting at 3:30 AM scheduling all other tasks relatively to his current time. And rescheduling himself for the next day at 3:30 AM.

With this scenario daylight savings are not an issue anymore.

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

If you don't have the luxury of being able to use Java 8, the following will do what you need:

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

It doesn't require any external libs, and will account for daylight savings. Simply pass in the time of day you want to run the task as a Calendar object, and the task as a Runnable. For example:

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

N.B. the timer task runs in a Daemon thread which is not recommended for doing any IO. If you need to use a User thread, you will need to add another method which cancels the timer.

If you have to use a ScheduledExecutorService, simply change the startTimer method to the following:

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

I am not sure of the behaviour but you may need a stop method which calls shutdownNow if you go down the ScheduledExecutorService route, otherwise your application may hang when you try to stop it.

Java8:
My upgrage version from top answer:

  1. Fixed situation when Web Application Server doens't want to stop, because of threadpool with idle thread
  2. Without recursion
  3. Run task with your custom local time, in my case, it's Belarus, 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");
}
}


}

You can use a simple date parse, if the time of the day is before now, let's start tomorrow :

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

Just to add up on Victor's answer.

I would recommend to add a check to see, if the variable (in his case the long midnight) is higher than 1440. If it is, I would omit the .plusDays(1), otherwise the task will only run the day after tomorrow.

I did it simply like this:

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

The following example work for me

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

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

You can use below class to schedule your task every day particular time

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

What if your server goes down at 4:59AM and comes back at 5:01AM? I think it will just skip the run. I would recommend persistent scheduler like Quartz, that would store its schedule data somewhere. Then it will see that this run hasn't been performed yet and will do it at 5:01AM.

Why to complicate a situation if you can just write like it? (yes -> low cohesion, hardcoded -> but it is a example and unfortunately with imperative way). For additional info read code example at below ;))

package timer.test;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.concurrent.*;


public class TestKitTimerWithExecuterService {


private static final Logger log = LoggerFactory.getLogger(TestKitTimerWithExecuterService.class);


private static final ScheduledExecutorService executorService
= Executors.newSingleThreadScheduledExecutor();// equal to => newScheduledThreadPool(1)/ Executor service with one Thread


private static ScheduledFuture<?> future; // why? because scheduleAtFixedRate will return you it and you can act how you like ;)






public static void main(String args[]){


log.info("main thread start");


Runnable task = () -> log.info("******** Task running ********");


LocalDateTime now = LocalDateTime.now();


LocalDateTime whenToStart = LocalDate.now().atTime(20, 11); // hour, minute


Duration duration = Duration.between(now, whenToStart);


log.info("WhenToStart : {}, Now : {}, Duration/difference in second : {}",whenToStart, now, duration.getSeconds());


future = executorService.scheduleAtFixedRate(task
, duration.getSeconds()    //  difference in second - when to start a job
,2                         // period
, TimeUnit.SECONDS);


try {
TimeUnit.MINUTES.sleep(2);  // DanDig imitation of reality
cancelExecutor();    // after canceling Executor it will never run your job again
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("main thread end");
}








public static void cancelExecutor(){


future.cancel(true);
executorService.shutdown();


log.info("Executor service goes to shut down");
}


}

Here, it is assumed that code is running on single VM. This scheduler code will run multiple times if the code is running on multiple VMs. That has to be handled separately.