• No results found

Scheduling recurring tasks in Java applications

N/A
N/A
Protected

Academic year: 2021

Share "Scheduling recurring tasks in Java applications"

Copied!
12
0
0

Loading.... (view fulltext now)

Full text

(1)

Introducing a simple generalisation of the Java language's

Timer class

Skill Level: Intermediate

Tom White

Lead Java Developer Kizoom

04 Nov 2003

All manner of Java applications commonly need to schedule tasks for repeated execution. Enterprise applications need to schedule daily logging or overnight batch processes. A J2SE or J2ME calendar application needs to schedule alarms for a user's appointments. However, the standard scheduling classes,Timerand

TimerTask, are not flexible enough to support the range of scheduling tasks

typically required. In this article, Java developer Tom White shows you how to build a simple, general scheduling framework for task execution conforming to an arbitrarily complex schedule.

Thejava.util.Timerandjava.util.TimerTask classes, which I'll refer to collectively as the Java timer framework, make it easy for programmers to schedule simple tasks. (Note that these classes are also available in J2ME.) Before this framework was introduced in the Java 2 SDK, Standard Edition, Version 1.3, developers had to write their own scheduler, which involved grappling with threads and the intricacies of theObject.wait() method. However, the Java timer framework is not rich enough to meet the scheduling requirements of many

applications. Even a task that needs repeating every day at the same time cannot be directly scheduled usingTimer, due to the time jumps that occur as daylight saving time comes and goes.

This article presents a scheduling framework that is a generalisation of Timerand

TimerTask to allow more flexible scheduling. The framework is very simple -- it consists of two classes and an interface -- and it's easy to learn. If you're used to

(2)

working with the Java timer framework, you should be able to pick up the scheduling framework very quickly. (For more information about the Java timer framework, see

Resources.)

Scheduling a one-shot task

The scheduling framework is built on top of the Java timer framework classes. Therefore, we'll first look at scheduling using these classes before I explain how the scheduling framework is used and how it is implemented.

Imagine an egg timer that tells you when a number of minutes have elapsed (and therefore that your egg is cooked) by playing a sound. The code in Listing 1 forms the basis for a simple egg timer written in the Java language:

Listing 1. EggTimer class

package org.tiling.scheduling.examples; import java.util.Timer;

import java.util.TimerTask; public class EggTimer {

private final Timer timer = new Timer(); private final int minutes;

public EggTimer(int minutes) { this.minutes = minutes; }

public void start() {

timer.schedule(new TimerTask() { public void run() {

playSound(); timer.cancel(); }

private void playSound() {

System.out.println("Your egg is ready!"); // Start a new thread to play a sound... }

}, minutes * 60 * 1000); }

public static void main(String[] args) { EggTimer eggTimer = new EggTimer(2); eggTimer.start();

} }

AnEggTimer instance owns aTimerinstance to provide the necessary

scheduling. When the egg timer is started using thestart()method, it schedules a

TimerTask to execute after the specified number of minutes. When the time is up, therun()method on theTimerTask is called behind the scenes byTimer, which causes it to play a sound. The application then terminates after the timer is

(3)

Scheduling a recurring task

Timerallows tasks to be scheduled for repeated execution by specifying a fixed rate of execution or a fixed delay between executions. However, there are many applications that have more complex scheduling requirements. For example, an alarm clock that sounds a wake-up call every morning at the same time cannot simply use a fixed rate schedule of 86400000 milliseconds (24 hours), because the alarm would be too late or early on the days the clocks go forward or backward (if your time zone uses daylight saving time). The solution is to use calendar arithmetic to calculate the next scheduled occurrence of a daily event. This is precisely what the scheduling framework supports. Consider theAlarmClockimplementation in Listing 2 (seeResourcesto download the source code for the scheduling framework, as well as a JAR file containing the framework and examples):

Listing 2. AlarmClock class

package org.tiling.scheduling.examples; import java.text.SimpleDateFormat; import java.util.Date; import org.tiling.scheduling.Scheduler; import org.tiling.scheduling.SchedulerTask; import org.tiling.scheduling.examples.iterators.DailyIterator; public class AlarmClock {

private final Scheduler scheduler = new Scheduler(); private final SimpleDateFormat dateFormat =

new SimpleDateFormat("dd MMM yyyy HH:mm:ss.SSS"); private final int hourOfDay, minute, second;

public AlarmClock(int hourOfDay, int minute, int second) { this.hourOfDay = hourOfDay;

this.minute = minute; this.second = second; }

public void start() {

scheduler.schedule(new SchedulerTask() { public void run() {

soundAlarm(); }

private void soundAlarm() {

System.out.println("Wake up! " +

"It's " + dateFormat.format(new Date())); // Start a new thread to sound an alarm... }

}, new DailyIterator(hourOfDay, minute, second)); }

public static void main(String[] args) {

AlarmClock alarmClock = new AlarmClock(7, 0, 0); alarmClock.start();

} }

(4)

Notice how similar the code is to the egg timer application. The AlarmClock

instance owns aScheduler instance (rather than aTimer) to provide the

necessary scheduling. When started, the alarm clock schedules a SchedulerTask

(rather than aTimerTask) to play the alarm. And instead of scheduling the task for execution after a fixed delay, the alarm clock uses aDailyIterator class to describe its schedule. In this case, it simply schedules the task at 7:00 AM every day. Here is the output from a typical run:

Wake up! It's 24 Aug 2003 07:00:00.023 Wake up! It's 25 Aug 2003 07:00:00.001 Wake up! It's 26 Aug 2003 07:00:00.058 Wake up! It's 27 Aug 2003 07:00:00.015 Wake up! It's 28 Aug 2003 07:00:00.002 ...

DailyIterator implementsScheduleIterator, an interface that specifies the scheduled execution times of aSchedulerTask as a series ofjava.util.Date

objects. Thenext()method then iterates over theDateobjects in chronological order. A return value ofnull causes the task to be cancelled (that is, it will never be run again) -- indeed, an attempt to reschedule will cause an exception to be thrown. Listing 3 contains theScheduleIteratorinterface:

Listing 3. ScheduleIterator interface

package org.tiling.scheduling; import java.util.Date;

public interface ScheduleIterator { public Date next();

}

DailyIterator'snext()method returnsDateobjects that represent the same time each day (7:00 AM), as shown in Listing 4. So if you call next()on a newly constructedDailyIterator class, you will get 7:00 AM of the day on or after the date passed into the constructor. Subsequent calls to next()will return 7:00 AM on subsequent days, repeating forever. To achieve this behavior DailyIterator

uses ajava.util.Calendar instance. The constructor sets up the calendar so that the first invocation ofnext()returns the correctDatesimply by adding a day onto the calendar. Note that the code contains no explicit reference to daylight saving time corrections; it doesn't need to because theCalendarimplementation (in this caseGregorianCalendar) takes care of this.

Listing 4. DailyIterator class

(5)

import org.tiling.scheduling.ScheduleIterator; import java.util.Calendar;

import java.util.Date; /**

* A DailyIterator class returns a sequence of dates on subsequent days * representing the same time each day.

*/

public class DailyIterator implements ScheduleIterator { private final int hourOfDay, minute, second;

private final Calendar calendar = Calendar.getInstance(); public DailyIterator(int hourOfDay, int minute, int second) {

this(hourOfDay, minute, second, new Date()); }

public DailyIterator(int hourOfDay, int minute, int second, Date date) { this.hourOfDay = hourOfDay; this.minute = minute; this.second = second; calendar.setTime(date); calendar.set(Calendar.HOUR_OF_DAY, hourOfDay); calendar.set(Calendar.MINUTE, minute); calendar.set(Calendar.SECOND, second); calendar.set(Calendar.MILLISECOND, 0); if (!calendar.getTime().before(date)) { calendar.add(Calendar.DATE, -1); } }

public Date next() {

calendar.add(Calendar.DATE, 1); return calendar.getTime(); }

}

Implementing the scheduling framework

In the previous section, we learned how to use the scheduling framework and

compared it with the Java timer framework. Next, I'll show you how the framework is implemented. In addition to theScheduleIteratorinterface shown inListing 3, there are two other classes --Scheduler andSchedulerTask-- that make up the framework. These classes actually useTimerandTimerTask under the covers, since a schedule is really no more than a series of one-shot timers. Listings 5 and 6 show the source code for the two classes:

Listing 5. Scheduler

package org.tiling.scheduling; import java.util.Date;

import java.util.Timer; import java.util.TimerTask; public class Scheduler {

(6)

private SchedulerTask schedulerTask; private ScheduleIterator iterator;

public SchedulerTimerTask(SchedulerTask schedulerTask, ScheduleIterator iterator) {

this.schedulerTask = schedulerTask; this.iterator = iterator;

}

public void run() { schedulerTask.run();

reschedule(schedulerTask, iterator); }

}

private final Timer timer = new Timer(); public Scheduler() {

}

public void cancel() { timer.cancel(); }

public void schedule(SchedulerTask schedulerTask, ScheduleIterator iterator) {

Date time = iterator.next(); if (time == null) {

schedulerTask.cancel(); } else {

synchronized(schedulerTask.lock) {

if (schedulerTask.state != SchedulerTask.VIRGIN) { throw new IllegalStateException("Task already scheduled " + "or cancelled");

}

schedulerTask.state = SchedulerTask.SCHEDULED; schedulerTask.timerTask =

new SchedulerTimerTask(schedulerTask, iterator); timer.schedule(schedulerTask.timerTask, time); }

} }

private void reschedule(SchedulerTask schedulerTask, ScheduleIterator iterator) {

Date time = iterator.next(); if (time == null) { schedulerTask.cancel(); } else { synchronized(schedulerTask.lock) { if (schedulerTask.state != SchedulerTask.CANCELLED) { schedulerTask.timerTask =

new SchedulerTimerTask(schedulerTask, iterator); timer.schedule(schedulerTask.timerTask, time); } } } } }

Listing 6 shows the source code for theSchedulerTask class: Listing 6. SchedulerTask

(7)

package org.tiling.scheduling; import java.util.TimerTask;

public abstract class SchedulerTask implements Runnable { final Object lock = new Object();

int state = VIRGIN;

static final int VIRGIN = 0; static final int SCHEDULED = 1; static final int CANCELLED = 2; TimerTask timerTask;

protected SchedulerTask() { }

public abstract void run(); public boolean cancel() {

synchronized(lock) {

if (timerTask != null) { timerTask.cancel(); }

boolean result = (state == SCHEDULED); state = CANCELLED;

return result; }

}

public long scheduledExecutionTime() { synchronized(lock) {

return timerTask == null ? 0 : timerTask.scheduledExecutionTime(); }

} }

Like the egg timer, every instance ofScheduler owns an instance ofTimerto provide the underlying scheduling. Instead of the single one-shot timer used to implement the egg timer,Scheduler strings together a chain of one-shot timers to execute aSchedulerTask class at the times specified by aScheduleIterator. Consider the publicschedule()method onScheduler -- this is the entry point for scheduling because it is the method a client calls. (The only other public method,

cancel(), is described inCanceling tasks.) The time of the first execution of the

SchedulerTask is discovered by callingnext()on theScheduleIterator

interface. The scheduling is then kicked off by calling the one-shot schedule()

method on the underlyingTimerclass for execution at this time. The TimerTask

object supplied for one-shot execution is an instance of the nested

SchedulerTimerTask class, which packages up the task and the iterator. At the allotted time, therun()method is called on the nested class, which uses the packaged task and iterator references to reschedule the next execution of the task. Thereschedule()method is very similar to theschedule()method, except that it is private and performs a slightly different set of state checks on SchedulerTask. The rescheduling process repeats indefinitely, constructing a new nested class instance for each scheduled execution, until the task or the scheduler is cancelled

(8)

(or the JVM shuts down).

Like its counterpartTimerTask,SchedulerTaskgoes through a series of states during its lifetime. When created, it is in aVIRGINstate, which simply means it has never been scheduled. Once scheduled, it shifts to a SCHEDULEDstate, then later to aCANCELLED state if the task is cancelled by one of the methods described below. Managing the correct state transitions, such as ensuring that a non-VIRGINtask is not scheduled twice, adds extra complexity to theScheduler andSchedulerTask

classes. Whenever an operation is performed that might change the state of the task, the code must synchronize on the task's lock object.

Canceling tasks

There are three ways to cancel a scheduled task. The first is to call the cancel()

method on theSchedulerTask. This is like callingcancel()on aTimerTask: the task will never run again, although it will run to completion if already running. The return value of thecancel() method is abooleanthat indicates whether further scheduled tasks might have run hadcancel() not been called. More precisely, it returnstrue if the task was in aSCHEDULEDstate immediately prior to calling

cancel(). If you try to reschedule a cancelled (or even scheduled) task,

Scheduler throws anIllegalStateException.

The second way to cancel a scheduled task is forScheduleIteratorto return

null. This is simply a shortcut for the first way, as the Schedulerclass calls

cancel() on theSchedulerTaskclass. Canceling a task this way is useful if you want the iterator -- rather than the task -- to control when the scheduling stops. The third way is to cancel the wholeScheduler by calling itscancel()method. This cancels all the scheduler's tasks and leaves it in a state where no more tasks may be scheduled on it.

Extending the cron facility

The scheduling framework could be likened to the UNIXcron facility, except that the specification of scheduling times is controlled imperatively rather than

declaratively. For example, theDailyIterator class used in the implementation ofAlarmClockhas the same scheduling as acron job, specified by acrontab

entry beginning0 7 * * *. (The fields specify minute, hour, day of month, month, and day of week, respectively.)

However, the scheduling framework has more flexibility thancron. Imagine a

HeatingControllerapplication that switches the hot water on in the mornings. I would like to instruct it to "turn the hot water on at 8:00 AM on weekdays and 9:00 AM on weekends." Using cron, I would need twocrontabentries (0 8 * * 1,2,3,4,5 and0 9 * * 6,7). By using aScheduleIterator, the solution is

(9)

more elegant because I can define a single iterator using composition. Listing 7 shows one way to do this:

Listing 7. Using composition to define a single iterator

int[] weekdays = new int[] { Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY };

int[] weekend = new int[] { Calendar.SATURDAY, Calendar.SUNDAY };

ScheduleIterator i = new CompositeIterator( new ScheduleIterator[] {

new RestrictedDailyIterator(8, 0, 0, weekdays), new RestrictedDailyIterator(9, 0, 0, weekend) }

);

ARestrictedDailyIterator class is likeDailyIterator, except it is

restricted to run on particular days of the week; and a CompositeIteratorclass takes a set ofScheduleIterators and correctly orders the dates into a single schedule. SeeResourcesfor the source code to these classes.

There are many other schedules thatcron cannot produce, but an implementation ofScheduleIteratorcan. For instance, the schedule described by "the last day of every month" can be implemented using standard Java calendar arithmetic (using theCalendar class), whereas it is impossible to express this usingcron.

Applications don't even have to use theCalendarclass. In the source code for this article (seeResources), I have included an example of a security light controller that runs to the schedule "turn the lights on 15 minutes before sunset." The

implementation uses the Calendrical Calculations Software Package (see

Resources) to compute the time of the sunset locally (given the latitude and longitude).

Real-time guarantees

When writing applications that use scheduling, it is important to understand what the framework promises in terms of timeliness. Will my tasks be executed early or late? If so, what's the maximum margin of error? Unfortunately, there are no simple answers to these questions. However, in practice the behavior is good enough for a large class of applications. The discussion below assumes that the system clock is correct (seeResourcesfor information on the Network Time Protocol).

BecauseScheduler delegates its scheduling to theTimerclass, the real-time guarantees thatScheduler can make are identical to those ofTimer.Timer

(10)

schedules tasks using theObject.wait(long)method. The current thread is made to wait until it is woken up, which can happen for one of these reasons:

1. Thenotify() ornotifyAll()method is called on the object by another thread.

2. The thread is interrupted by another thread.

3. The thread is woken up in the absence of a notify (known as a spurious wakeup, described in Item 50 of Joshua Bloch's Effective Java

Programming Language Guide -- seeResources). 4. The specified amount of time has elapsed.

The first possibility cannot occur for theTimerclass because the object that

wait()is called on is private. Even so, the implementation ofTimersafeguards against the first three causes of early wakeup, thus ensuring that the thread wakes up after the time has elapsed. Now, the documentation comment for

Object.wait(long)states it may wake up after the time has elapsed "more or less", so it is possible that the thread wakes up early. In this case,Timerissues anotherwait()for(scheduledExecutionTime

-System.currentTimeMillis())milliseconds, thereby guaranteeing that tasks can never be executed early.

Can tasks be executed late? Yes. There are two main causes of late execution: thread scheduling and garbage collection.

The Java language specification is purposefully vague on thread scheduling. This is because the Java platform is general purpose and targets a wide range of hardware and associated operating systems. While most JVM implementations have a thread scheduler that is fair, it is by no means guaranteed -- certainly implementations have different strategies for allocating processor time to threads. Therefore, when a

Timerthread wakes up after its allotted time, the time at which it actually executes its task depends on the JVM's thread scheduling policy, as well as how many other threads are contending for processor time. Therefore, to mitigate late task execution, you should minimize the number of runnable threads in your application. It is worth considering running schedulers in a separate JVM to achieve this.

The time that the JVM spends performing garbage collection (GC) can be significant for large applications that create lots of objects. By default, when GC occurs the whole application must wait for it to finish, which may take several seconds or more. (The command line option-verbose:gcfor thejavaapplication launcher will cause each GC event to be reported to the console.) To minimize pauses due to GC, which may hinder prompt task execution, you should minimize the number of objects your application creates. Again, this is helped by running your scheduling code in a separate JVM. Also, there are a number of tuning options that you can try

(11)

to minimize GC pauses. For instance, incremental GC attempts to spread the cost of the major collections over several minor collections. The trade-off is that this reduces the efficiency of GC, but this might be an acceptable price for timelier scheduling. (SeeResourcesfor more GC tuning hints.)

When was I scheduled?

To determine whether tasks are being run in a timely manner, it helps if the tasks themselves monitor and record any instances of late execution. SchedulerTask, likeTimerTask, has ascheduledExecutionTime()method that returns the time that the most recent execution of this task was scheduled to occur. Evaluating the expressionSystem.currentTimeMillis()

-scheduledExecutionTime() at the beginning of the task'srun()method lets you determine how late the task was executed, in milliseconds. This value could be logged to produce statistics on the distribution of late execution. The value might also be used to decide what action the task should take -- for example, if the task is too late, it might do nothing. If, after following the guidelines above, your application requires stricter guarantees of timeliness, consider looking at the Real-time

Specification for Java (seeResourcesfor more information).

Conclusion

In this article, I have introduced a simple enhancement to the Java timer framework that permits very flexible scheduling strategies. The new framework is essentially a generalisation ofcron -- in fact, it would be valuable to implementcronas a

ScheduleIteratorinterface to provide a pure Javacronreplacement. While not offering strict real-time guarantees, the framework is applicable to a host of general purpose Java applications that need to schedule tasks on a regular basis.

(12)

Downloads

Description Name Size Download

method

Sample code for article j-schedule.zip 225KB HTTP

Information about download methods

About the author

Tom White

Tom White is Lead Java Developer at Kizoom, a leading UK software company in the delivery of personalised travel information to mobile devices. Clients include the UK's national train operator, the London public transport system, and UK national bus companies. Since its founding in 1999, Kizoom has used all the disciplines from Extreme Programming. Tom has been writing Java programs full time since 1996, using most of the standard and enterprise Java APIs, from client Swing GUIs and graphics to back-end messaging systems. He has a first class honours degree in mathematics from Cambridge University. When not programming, Tom enjoys

making his young daughter laugh and watching 1930s Hollywood films. Contact Tom attom-at-tiling.org.

References

Related documents

public class Square implements Surfaceable { private final double side ;. public Square(double side) {

1995 Northeastern Forest Experiment Station and West Virginia University 1997 University of Missouri and the North Central Forest Experiment Station 1999 University of Kentucky and

The aim of this study was to assess the occlusal relationships of primary dentition of Korean preschool children in Hwaseong city.. Total 444 children of age group between 2 and

public class XorSocket extends Socket { private final byte pattern;}. private InputStream in

class TextApplet extends Controller implements ActionListener { private static final int ROWS = 10; // rows in TextArea private static final int COLS = 30; // cols in text field

public static class Map extends MapReduceBase implements Mapper<LongWritable, Text, Text, IntWritable> { private final static IntWritable one = new IntWritable(1);. private

Open while scheduling a recurring zoom meetings on your data for a waiting room controller, allowing your outlook calendar!. Let people sometimes a new google calendar app and calls

Strategic Management 2 1 2 2 2 3 3 3 2 2 3 2 2 2 May, September April, August February, June March, July April, September March, July March, August April, October May, November