USING TIMERS TO RUN RECURRING
OR FUTURE TASKS ON A
BACKGROUND THREAD
Many applications need to schedule tasks for future execution, or
to schedule them to recur at a regular interval. J2SE v1.3 meets
this need with the addition of two Timer classes: java.util.Timer
and java.util.TimerTask. This tip demonstrates various scheduling
strategies for using these Timer classes. The tip also shows you
how to handle poorly-behaved tasks, that is, tasks that run too
long or that crash.
The java.util.Timer and java.util.TimerTask classes are simple to
use. As with many things threaded, the TimerTask class implements
the Runnable interface. To use the class, simply write a subclass
with a run method that does the work; then plug the subclass into
a Timer instance. Here's an example:
import java.util.*;
import java.io.*;
public class TestTimers {
public static void doMain() throws Exception {
Timer t = new Timer(true);
t.schedule(new Ping("Fixed delay"), 0, 1000);
Thread.currentThread().sleep(12000);
}
public static void main(String[] args) {
try {
doMain();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
class Ping extends TimerTask {
private String name;
public Ping(String name) {
this.name = name;
}
public void run() {
System.out.println(name + " Ping at " + new Date());
}
}
The class TestTimers creates a Timer. By passing the Timer the
boolean value true, TestTimers forces the Timer to use a daemon
thread. The main thread then sleeps, allowing you see the Timer
at work. However you never actually see any thread classes or
instances; those details are encapsulated by the Timer class.
In the statement,
t.schedule(new Ping("Fixed delay"), 0, 1000);
the parameters to the schedule method cause a Ping object's run
method to be invoked after an initial delay of 0 milliseconds;
the method is repeatedly invoked every 1000 milliseconds. Ping's
run method logs output to System.out. (In your own applications,
you would use the run method to do something more interesting.)
If you run TestTimers, you will see output similar to this:
Fixed delay ping at Thu May 18 14:18:56 EDT 2000
Fixed delay ping at Thu May 18 14:18:57 EDT 2000
Fixed delay ping at Thu May 18 14:18:58 EDT 2000
Fixed delay ping at Thu May 18 14:18:59 EDT 2000
Fixed delay ping at Thu May 18 14:19:00 EDT 2000
Fixed delay ping at Thu May 18 14:19:01 EDT 2000
Fixed delay ping at Thu May 18 14:19:02 EDT 2000
Fixed delay ping at Thu May 18 14:19:03 EDT 2000
Fixed delay ping at Thu May 18 14:19:04 EDT 2000
Fixed delay ping at Thu May 18 14:19:05 EDT 2000
Fixed delay ping at Thu May 18 14:19:06 EDT 2000
Fixed delay ping at Thu May 18 14:19:07 EDT 2000
Fixed delay ping at Thu May 18 14:19:08 EDT 2000
The output confirms that Ping is running about once per second,
exactly as requested. Better still, the Timer can handle multiple
TimerTasks, each with different start times and repeat periods.
This leads to an interesting question: If a TimerTask takes a very
long time to complete, will other tasks in the list be thrown off?
To answer this question, you need to understand how the Timer uses
threads. Each Timer instance has a single dedicated thread that
all the TimerTasks share. So, if one task takes a long time, all
the other tasks wait for it to complete. Consider this long-running
task:
class PainstakinglySlowTask extends TimerTask {
public void run() {
file://simulate some very slow activity by sleeping
try {
Thread.currentThread().sleep(6000);
System.out.println("Painstaking task ran at " + new Date());
}
catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
The PainstakinglySlowTask class sleeps for six full seconds. It
prevents any other tasks from executing during that time. What
happens if you add the painstakingly slow task to TestTimers?
Let's see.
public static void doMain() throws Exception {
Timer t = new Timer(true);
t.schedule(new Ping("Fixed delay"), 0, 1000);
t.schedule(new PainstakinglySlowTask(), 2000);
Thread.currentThread().sleep(12000);
}
If you recompile and run TestTimers, you will see output like this:
Fixed delay Ping at Thu May 18 15:41:33 EDT 2000
Fixed delay Ping at Thu May 18 15:41:34 EDT 2000
Fixed delay Ping at Thu May 18 15:41:35 EDT 2000
Painstaking task ran at Thu May 18 15:41:41 EDT 2000
Fixed delay Ping at Thu May 18 15:41:41 EDT 2000
Fixed delay Ping at Thu May 18 15:41:42 EDT 2000
Fixed delay Ping at Thu May 18 15:41:43 EDT 2000
Fixed delay Ping at Thu May 18 15:41:44 EDT 2000
Fixed delay Ping at Thu May 18 15:41:45 EDT 2000
During the time that PainstakinglySlowTask runs (from 15:41:35
to 15:41:41), no pings occur. This is what is meant by a "fixed
delay". The Timer tries to make the delay between Pings as
precise as possible, even if that means that some Pings are lost
during the running time of another, long-running task.
A scheduling alternative is "fixed rate." With fixed rate
scheduling, the Timer tries to make the processing rate as
accurate as possible over time. So, if one task runs for a long
time, other tasks can instantaneously run several times in order
to catch up. You can specify fixed rate scheduling by using the
scheduleAtFixedRate method:
file://commented out the fixed delay version
file://t.schedule(new Ping("Fixed delay"), 0, 1000);
t.scheduleAtFixedRate(new Ping("Fixed rate"), 0, 1000);
t.schedule(new PainstakinglySlowTask(), 2000);
If you run TestTimers with a fixed rate ping, you should
see output like this:
Fixed rate Ping at Thu May 18 15:48:33 EDT 2000
Fixed rate Ping at Thu May 18 15:48:34 EDT 2000
Fixed rate Ping at Thu May 18 15:48:35 EDT 2000
Painstaking task ran at Thu May 18 15:48:41 EDT 2000
Fixed rate Ping at Thu May 18 15:48:41 EDT 2000
Fixed rate Ping at Thu May 18 15:48:41 EDT 2000
Fixed rate Ping at Thu May 18 15:48:41 EDT 2000
Fixed rate Ping at Thu May 18 15:48:41 EDT 2000
Fixed rate Ping at Thu May 18 15:48:41 EDT 2000
Fixed rate Ping at Thu May 18 15:48:41 EDT 2000
Fixed rate Ping at Thu May 18 15:48:42 EDT 2000
Fixed rate Ping at Thu May 18 15:48:43 EDT 2000
Fixed rate Ping at Thu May 18 15:48:44 EDT 2000
Fixed rate Ping at Thu May 18 15:48:45 EDT 2000
This time, several Pings run right after PainstakinglySlowTask
finishes; the Pings all run at 15:48:41. This keeps the rate of
Pings as close as possible to the desired 1000 msec average. The
price paid is occasionally having Pings run at approximately the
same time.
Both fixed-rate and fixed-delay scheduling have their uses.
However, neither totally eliminates the interference caused by
long-running tasks. If you have different tasks that might run
for a very long time, you might want to minimize the interference
between the tasks. This is especially true if you need to take
advantage of multiple CPUs. A single Timer provides no obvious
way to do this. You cannot control the Timer thread, because it
is encapsulated as a private field of the Timer class. Instead,
you can create multiple Timers, or have one Timer call notify()
and have other threads do the actual work.
Tasks that throw exceptions pose more of a problem than
long-running tasks. Here's an example. Replace the
PainstakinglySlowTask class with the following CrashingTask class:
class CrashingTask extends TimerTask {
public void run() {
throw new Error("CrashingTask");
}
}
file://new version of TestTimers
public static void doMain() throws Exception {
Timer t = new Timer(true);
t.scheduleAtFixedRate(new Ping("Fixed rate"), 0, 1000);
t.schedule(new CrashingTask(), 5000, 1000);
Thread.currentThread().sleep(12000);
}
If you run TestTimers with CrashingTask, you should see output
that looks something like this:
Fixed rate Ping at Thu May 18 15:58:53 EDT 2000
Fixed rate Ping at Thu May 18 15:58:54 EDT 2000
Fixed rate Ping at Thu May 18 15:58:55 EDT 2000
Fixed rate Ping at Thu May 18 15:58:56 EDT 2000
Fixed rate Ping at Thu May 18 15:58:57 EDT 2000
Fixed rate Ping at Thu May 18 15:58:58 EDT 2000
java.lang.Error: CrashingTask
at CrashingTask.run(TestTimers.java:37)
at java.util.TimerThread.mainLoop(Timer.java:435)
at java.util.TimerThread.run(Timer.java:385)
After CrashingTask throws an exception, it never runs again. This
should come as no surprise. What may surprise you is that no other
task on the same Timer will run again, either. A wayward
exception will cancel the Timer, causing any future attempt to
schedule a task to throw an exception. However, there is no
mechanism to notify your existing tasks that they have been
brutally de-scheduled. It is up to you to make sure that errant
TimerTasks do not destroy your Timers. One strategy is to guarantee
that your TimerTasks never throw exceptions back into the Timer.
You can do this by enclosing the TimerTasks in a try block that
catches the exception. If you need to be notified of the exception,
you can create a simple mechanism to notify the program that
a failure occurred. Here's an example:
import java.util.*;
import java.io.*;
interface ExceptionListener {
public void exceptionOccurred(Throwable t);
}
class ExceptionLogger implements ExceptionListener {
public void exceptionOccurred(Throwable t) {
System.err.println("Exception on Timer thread!");
t.printStackTrace();
}
}
public class TestTimers {
public static void doMain() throws Exception {
Timer t = new Timer(true);
file://t.schedule(new Ping("Fixed delay"), 0, 1000);
t.scheduleAtFixedRate(new Ping("Fixed rate"), 0, 1000);
t.schedule(new CrashingTask(new ExceptionLogger()), 5000, 5000);
file://t.schedule(new PainstakinglySlowTask(), 2000);
Thread.currentThread().sleep(12000);
}
public static void main(String[] args) {
try {
doMain();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
class Ping extends TimerTask {
private String name;
public Ping(String name) {
this.name = name;
}
public void run() {
System.out.println(name + " Ping at " + new Date());
}
}
class CrashingTask extends TimerTask {
ExceptionListener el;
public CrashingTask(ExceptionListener el) {
this.el = el;
}
public void run() {
try {
throw new Error("CrashingTask");
}
catch (Throwable t) {
cancel();
el.exceptionOccurred(t);
}
}
}
This code is very similar to the previous version, except that this
time CrashingTask's run method never propagates exceptions of any
type. Instead, it uses a catch block to catch all Throwables and
then uses a callback interface to report the exception. Here's the
output:
Fixed rate Ping at Thu May 18 16:41:03 EDT 2000
Fixed rate Ping at Thu May 18 16:41:04 EDT 2000
Fixed rate Ping at Thu May 18 16:41:05 EDT 2000
Fixed rate Ping at Thu May 18 16:41:06 EDT 2000
Fixed rate Ping at Thu May 18 16:41:07 EDT 2000
Fixed rate Ping at Thu May 18 16:41:08 EDT 2000
Exception on Timer thread!
java.lang.Error: CrashingTask
at CrashingTask.run(TestTimers.java:54)
at java.util.TimerThread.mainLoop(Timer.java:435)
at java.util.TimerThread.run(Timer.java:385)
Fixed rate Ping at Thu May 18 16:41:09 EDT 2000
Fixed rate Ping at Thu May 18 16:41:10 EDT 2000
Fixed rate Ping at Thu May 18 16:41:11 EDT 2000
Fixed rate Ping at Thu May 18 16:41:12 EDT 2000
Fixed rate Ping at Thu May 18 16:41:13 EDT 2000
Fixed rate Ping at Thu May 18 16:41:14 EDT 2000
Fixed rate Ping at Thu May 18 16:41:15 EDT 2000
When CrashingTask throws an exception, it calls cancel on itself
to remove itself from the Timer. It then logs the exception by
calling an implementation of the ExceptionListener interface.
Because the exception never propagates back into the Timer thread,
the Pings continue to function even after CrashingTask fails. In
a production system, a more robust implementation of
ExceptionListener could take action to deal with the exception
instead of simply logging it.
There is another Timer class in the Java Platform,
javax.swing.Timer. Which Timer should you use? The Swing Timer is
designed for a very specific purpose. It does work on the AWT event
thread. Because much of the Swing package code must execute on the
AWT event thread, you should use the Swing Timer if you are
manipulating the user interface. For other tasks, use the
java.util.Timer for its flexible scheduling.
For more info on the Timer classes, see
http://java.sun.com/j2se/1.3/docs/api/java/util/Timer.html.
|