|
|
|
Contents: |
|
|
|
Related content: |
|
|
|
Subscriptions: |
|
|
| Introducing a simple generalisation of the Java language's
Timer class
Tom
White Lead Java Developer, Kizoom 4 November 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,
Timer and 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.
The java.util.Timer and java.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 the Object.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 using
Timer , 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 Timer and 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 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();
}
}
|
An EggTimer instance owns a Timer instance to
provide the necessary scheduling. When the egg timer is started using the
start() method, it schedules a TimerTask to
execute after the specified number of minutes. When the time is up, the
run() method on the TimerTask is called behind
the scenes by Timer , which causes it to play a sound. The
application then terminates after the timer is cancelled.
Scheduling a recurring
task
Timer allows 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
the AlarmClock implementation in Listing 2 (see Resources to
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();
}
}
|
Notice how similar the code is to the egg timer application. The
AlarmClock instance owns a Scheduler instance
(rather than a Timer ) to provide the necessary scheduling.
When started, the alarm clock schedules a SchedulerTask
(rather than a TimerTask ) to play the alarm. And instead of
scheduling the task for execution after a fixed delay, the alarm clock
uses a DailyIterator 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 implements ScheduleIterator , an
interface that specifies the scheduled execution times of a
SchedulerTask as a series of java.util.Date
objects. The next() method then iterates over the
Date objects in chronological order. A return value of
null 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 the ScheduleIterator
interface: Listing 3. ScheduleIterator
interface
package org.tiling.scheduling;
import java.util.Date;
public interface ScheduleIterator {
public Date next();
}
|
DailyIterator 's next() method returns
Date objects that represent the same time each day (7:00 AM),
as shown in Listing 4. So if you call next() on a newly
constructed DailyIterator 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 a
java.util.Calendar instance. The constructor sets up the
calendar so that the first invocation of next() returns the
correct Date simply 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 the Calendar
implementation (in this case GregorianCalendar ) takes care of
this. Listing 4. DailyIterator class
package org.tiling.scheduling.examples.iterators;
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 the
ScheduleIterator interface shown in Listing 3, there
are two other classes -- Scheduler and
SchedulerTask -- that make up the framework. These classes
actually use Timer and TimerTask 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 {
class SchedulerTimerTask extends TimerTask {
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 the SchedulerTask
class: Listing 6. SchedulerTask
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 of Scheduler owns an
instance of Timer to 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 a SchedulerTask class at the times specified by a
ScheduleIterator .
Consider the public schedule() method on
Scheduler -- this is the entry point for scheduling because
it is the method a client calls. (The only other public method,
cancel() , is described in Canceling
tasks.) The time of the first execution of the
SchedulerTask is discovered by calling next() on
the ScheduleIterator interface. The scheduling is then kicked
off by calling the one-shot schedule() method on the
underlying Timer class 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, the
run() method is called on the nested class, which uses the
packaged task and iterator references to reschedule the next execution of
the task. The reschedule() method is very similar to the
schedule() 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 (or the JVM shuts down).
Like its counterpart TimerTask , SchedulerTask
goes through a series of states during its lifetime. When created, it is
in a VIRGIN state, which simply means it has never been
scheduled. Once scheduled, it shifts to a SCHEDULED state,
then later to a CANCELLED state if the task is cancelled by
one of the methods described below. Managing the correct state
transitions, such as ensuring that a non-VIRGIN task is not
scheduled twice, adds extra complexity to the Scheduler and
SchedulerTask 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 the SchedulerTask . This is
like calling cancel() on a TimerTask : the task
will never run again, although it will run to
completion if already running. The return value of the
cancel() method is a boolean that indicates
whether further scheduled tasks might have run had cancel()
not been called. More precisely, it returns true if the task
was in a SCHEDULED state immediately prior to calling
cancel() . If you try to reschedule a cancelled (or even
scheduled) task, Scheduler throws an
IllegalStateException .
The second way to cancel a scheduled task is for
ScheduleIterator to return null . This is simply
a shortcut for the first way, as the Scheduler class calls
cancel() on the SchedulerTask class. 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 whole Scheduler by calling
its cancel() 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
UNIX cron facility, except that the specification of
scheduling times is controlled imperatively rather than declaratively. For
example, the DailyIterator class used in the implementation
of AlarmClock has the same scheduling as a cron
job, specified by a crontab entry beginning 0 7 * *
* . (The fields specify minute, hour, day of month, month, and day
of week, respectively.)
However, the scheduling framework has more flexibility than
cron . Imagine a HeatingController application
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 two crontab
entries (0 8 * * 1,2,3,4,5 and 0 9 * * 6,7 ). By
using a ScheduleIterator , the solution is 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)
}
);
|
A RestrictedDailyIterator class is like
DailyIterator , except it is restricted to run on particular
days of the week; and a CompositeIterator class takes a set
of ScheduleIterator s and correctly orders the dates into a
single schedule. See Resources for
the source code to these classes.
There are many other schedules that cron cannot produce,
but an implementation of ScheduleIterator can. For instance,
the schedule described by "the last day of every month" can be implemented
using standard Java calendar arithmetic (using the Calendar
class), whereas it is impossible to express this using cron .
Applications don't even have to use the Calendar class. In
the source code for this article (see Resources), 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 (see Resources for
information on the Network Time Protocol).
Because Scheduler delegates its scheduling to the
Timer class, the real-time guarantees that
Scheduler can make are identical to those of
Timer . Timer schedules tasks using the
Object.wait(long) method. The current thread is made to wait
until it is woken up, which can happen for one of these reasons:
- The
notify() or notifyAll() method is
called on the object by another thread.
- The thread is interrupted by another thread.
- 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 -- see Resources).
- The specified amount of time has elapsed.
The first possibility cannot occur for the Timer class
because the object that wait() is called on is private. Even
so, the implementation of Timer safeguards 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, Timer issues another wait() 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
Timer thread 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:gc for the java application 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 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. (See Resources for
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 , like TimerTask , has a
scheduledExecutionTime() method that returns the time that
the most recent execution of this task was scheduled to occur. Evaluating
the expression System.currentTimeMillis() -
scheduledExecutionTime() at the beginning of the task's
run() 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 (see Resources for
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 of cron -- in fact,
it would be valuable to implement cron as a
ScheduleIterator interface to provide a pure Java
cron replacement. 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.
Resources
- Download the source code
used in this article.
- "Tuning Garbage
Collection with the 1.3.1 Java Virtual Machine" is a very useful
article from Sun with hints on how to minimize your GC pauses.
- For even more information on GC from developerWorks,
see the following articles:
- In "Java theory and
practice: Concurrency made simple (sort of)" (developerWorks,
November 2002) Brian Goetz discusses Doug Lea's
util.concurrent library, a treasure trove of concurrency
utility classes.
- Also by Brian Goetz, "Threading lightly,
Part 2: Reducing contention" (developerWorks,
September 2001) examines thread contention and how to reduce it.
- Here's the documentation for Timer and TimerTask in
the Java SDK 1.4.
- Jcrontab is
another scheduler written in the Java language, with the emphasis on
replacing
cron .
- The Network Time
Protocol (NTP) is a protocol for synchronizing computer
clocks.
- The Real-time
Specification for Java is an extension to the Java platform that
provides real-time guarantees. This site also includes a reference
implementation.
- Effective Java
Programming Language Guide by Joshua Bloch (Addison-Wesley,
2001) contains excellent advice (for instance, Item 51: Don't depend on
the thread scheduler).
- For everything you want to know about the world's calendars and the
algorithms to work with them, see Calendrical
Calculations by Nachum Dershowitz and Edward M. Reingold
(Cambridge University Press, 1997).
- Chapter 17, Threads and Locks, of The Java
Language Specification, Second Edition by James Gosling, Bill
Joy, Guy L. Steele Jr., and Gilad Bracha (Addison-Wesley, 2000)
specifies the behavior of the thread scheduler.
- Concurrent
Programming in Java: Design Principles and Patterns by Doug Lea
(Addison-Wesley, 1999) is a great book that delves into all aspects of
concurrency in the Java platform.
- You'll find hundreds of articles about every aspect of Java
programming in the developerWorks
Java technology zone.
About the
author 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 at tom-at-tiling.org. |
|
|