In the previous chapter, we examined an interesting aspect of threads. Before we used a thread pool, we were concerned with creating, controlling, and communicating between threads. With a thread pool, we were concerned with the task that we wanted to execute. Using an executor allowed us to focus on our program’s logic instead of writing a lot of thread-related code.
In this chapter, we examine this idea in another context. Task schedulers give us the opportunity to execute particular tasks at a fixed point in time in the future (or, more correctly, after a fixed point in time in the future). They also allow us to set up repeated execution of tasks. Once again, they free us from many of the low-level details of thread programming: we create a task, hand it off to a task scheduler, and don’t worry about the rest.
Java provides different kinds of task schedulers. Timer classes execute tasks (perhaps repeatedly) at a point in the future. These classes provide a basic task scheduling feature. J2SE 5.0 has a new, more flexible task scheduler that can be used to handle many tasks more effectively than the timer classes. In this chapter, we’ll look into all of these classes.
Interestingly, this is not the first time that we have been concerned with when a task is to be executed. Previously, we’ve just considered the timing as part of the task. We’ve seen tools that allow threads to wait for specific periods of time. Here is a quick review:
sleep()
methodIn our discussion of the Thread
class, we examined the concept of
a thread waiting for a specific period of time. The purpose was
either to allow other threads to accomplish related tasks, to
allow external events to happen during the sleeping period, or to
repeat a task periodically. The tasks that are listed after the
sleep()
method are executed at
a later time period. In effect, the sleep()
method controls when those tasks
are executed.
join()
methodOur discussion of this method of the Thread
class represents the first time
that we examined alternate tasks to be executed at a later time.
The goal of this method is to wait for a specific event—a thread
termination. However, the expected thread termination event may
not arrive, at least not within the desired time period, so the
join()
method provides a
timeout. This allows the method to return—either by the
termination of the thread or by the expiration of the timeout—thus
allowing the program to execute an alternate task at a specific
time and in a particular situation.
wait()
methodThe wait()
method of the
Object
class allows a thread to
wait for any event. This method also provides the option to return
if a specific time period passes. This allows the program to
execute a task at a later time if the event occurs or to specify
the exact time to execute an alternate task if the event does not
occur. This functionality is also emulated with condition
variables using the await()
method.
TimeUnit
classThis class is used to define a time period, allowing methods
to specify a time period in units other than milliseconds or
nanoseconds. This class is used by many of the classes added in
J2SE 5.0 to specify a time period for a timeout. This class also
provides convenience methods to support certain periodic
requests—specifically, it provides alternate implementations of
the sleep()
, join()
, and wait()
methods that use a TimeUnit
object as their timeout
argument.
DelayQueue
classOur discussion of the DelayQueue
class in Chapter 8 is the first time we
encounter a class that allows data to be processed at a specific
time. When a producer places data in a delay queue, it is not
readable by consumers until after a specific period passes. In
effect, the task to process the data is to be executed at a later
time—a time period that is specified by the data itself.
As these examples show, in some cases, a program needs to execute code only after a specific event or after a period of time. Much of the time, the functionality is indirect in that the timeout is not expected to occur. Java also supports timeout functions directly by providing tools that allow the program to execute specific tasks at a specific time.
We’ve used these methods in our examples when a program needs to execute code only after a specific event or after a period of time. The timing in these cases has always been provided as a timeout value: after a certain period of time, the thread would regain control and be able to execute the appropriate task. However, in this case control always resides with the thread: execution of the appropriate task is synchronous with respect to the code being executed. Java also supports asynchronous task execution in alternate threads; it’s that type of execution that we’ll examine in the remainder of this chapter.
The java.util.Timer
class was added to JDK 1.3 specifically to provide a
convenient way for tasks to be executed asynchronously. This class
allows an object (of a specific class we’ll look at) to be executed at a
later time. The time can be specified either relative to the current
time or as an absolute time. This class also supports the repeated
execution of the task.
The Timer
class executes tasks
with a specific interface:
public abstract class TimerTask implements Runnable { protected TimerTask( ); public abstract void run( ); public boolean cancel( ); public long scheduledExecutionTime( ); }
Tasks to be executed by the Timer
class must inherit from the TimerTask
class. As in the Thread
class, the task to be executed is the
run()
method. In fact, the TimerTask
class actually implements the
Runnable
interface. The Timer
class requires a TimerTask
object so that two methods can be
attached to the task; these methods can be used to maintain the task.
These methods do not have to be implemented; the TimerTask
class provides a default
implementation for them. A class that inherits from the TimerTask
class need only implement the
run()
method.
The downside of this technique is that the task can’t inherit from
other classes. Since the TimerTask
class is not an interface, it means that tasks have to either be created
from classes that don’t already inherit from other classes, or wrapper
classes have to be created to forward the request.
The cancel()
method is used to stop the class from being executed. A
task that is already executing is unaffected when this method is called.
However, if the task is repeating, calling the cancel()
method prevents further execution of
the class. For tasks that are executed only once, the cancel( )
method returns whether the task has
been cancelled: if the task is currently running, has already run, or
has been previously cancelled, it returns a boolean value of false
. For repeating tasks, this method always
returns a boolean value of true
.
The scheduledExecutionTime()
method is used to return the time at which the previous
invocation of a repeating task occurred. If the task is currently
running, it is the time at which the task began. If the task is not
running, it is the time at which the previous execution of the task
began. Its purpose is a bit obscure but it will make more sense after we
discuss the Timer
class.
Here is the interface of the Timer
class:
public class Timer { public Timer( ); public Timer(boolean isDaemon); public Timer(String name); public Timer(String name, boolean isDaemon); public void schedule(TimerTask task, long delay); public void schedule(TimerTask task, Date time); public void schedule(TimerTask task, long delay, long period); public void schedule(TimerTask task, Date firstTime, long period); public void scheduleAtFixedRate(TimerTask task, long delay, long period); public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period); public void cancel( ); public int purge( ); }
The Timer
class provides the
means to execute tasks at a later time. The tasks that are scheduled are
placed in an ordered queue and are executed sequentially by a single
thread.
Four constructors are provided to create different versions of the
Timer
class. The most important
parameter of these constructors allows the definition of whether the
created thread is a daemon thread (see Chapter 13). This is useful for tasks
which are needed only if the user is still interacting with the program.
If the timer thread is a daemon thread, the program can exit when all
the user threads terminate. The other parameter is used to name the
thread; this is important if the threads are to be monitored by a
debugger.
The first two overloaded versions of the schedule()
method are used to schedule
one-time tasks. The first allows for the specification of a delay: a
time period in milliseconds relative to the current time. The second
allows for the specification of an absolute time.
The last two overloaded versions of the schedule()
method are used to schedule repeating tasks. The third
parameter is used to specify the period in milliseconds between
invocations of the repeated tasks.
There are a few important issues in the timer implementation,
particularly for repeated tasks. First, only a single thread executes
the tasks. While it is recommended that the tasks executed by the
Timer
class be short-lived, no check
ensures that this is so. This means that if the Timer
object is overwhelmed, a task may be
executed at a time much later than the specified time. For repeated
tasks, the schedule()
method does not
take this into account. The schedule time is allowed to drift, meaning
that the next iteration of the task is based on the previous iteration.
This is not very useful if the task is used to maintain a clock or other
time-critical task.
Two mechanisms can be used to resolve this. The first mechanism is
the two overloaded scheduleAtFixedRate()
methods. The schedule()
method schedules the next execution
of the task based on when the previous execution actually occurred. The
next iteration of the task scheduled by the scheduleAtFixedRate()
method is calculated
from when the previous iteration was supposed to
execute—not when the previous iteration actually executes.
The second mechanism is the scheduledExecutionTime()
method of the
TimerTask
class. This method can be
used by the task itself to determine when the task is supposed to run.
Based on the comparison to the current time, the task can adjust its
behavior. This is even more important when the scheduleAtFixedRate()
method is used to
schedule the task. Since the tasks are not allowed to drift, more than
one iteration of the repeated task may be waiting to execute. As a
result, a timer task may want to skip a particular execution if it knows
that another execution is pending in the queue. For example, a task that
runs every five seconds can tell if it has missed an execution by using
this code:
public class MyTimerTask extends TimerTask { public void run( ) { if (System.currentTimeMillis( ) - scheduledExecutionTime( ) > 5000) { // We're more than five seconds off; skip this because another task // will already have been scheduled. return; } ... } }
Table 11-1 shows
when tasks would be executed under different scheduling models of the
Timer
class. In this example, we’re
assuming that the task is to be run every second, executes for .1
seconds, and the system becomes bogged down for .5 seconds between the
second and third iteration. The schedule()
method drifts by .5 seconds on
subsequent executions. The scheduleAtFixedRate()
method runs the delayed
iteration .5 seconds late but still executes the remaining iterations
according to the original schedule. Neither takes into account the time
required to execute the task.
The cancel()
method is provided
by the Timer
class to destroy the
timer. All the tasks in the timer are simply cancelled, and no new tasks
are allowed to be scheduled. The Timer
object can no longer be used to schedule
any more tasks. If a task is currently executing, it is allowed to
finish; currently executing tasks are not interrupted.
The purge()
method is used for
maintenance. The task’s cancel()
method does not actually delete the task from the task queue; the task
is simply marked as cancelled. The task is deleted from the queue by the
timer when it is time for the task to execute: because the task is
marked as cancelled, the task is skipped and deleted from the queue at
that time. The purge()
method is
important only when a large number of tasks are being cancelled (or the
tasks themselves consume a lot of memory). By purging the timer, the
task objects are removed from the queue, allowing them to be garbage collected.
Here’s an example that uses the Timer
class. The example program allows you
to monitor the reachability of one or more web sites: it periodically
attempts to retrieve a URL from each web site. Web sites that are
reachable are displayed in green; web sites that are down are
displayed in red.
We start with the timer task that contacts the web site:
package javathreads.examples.ch11.example1; import java.util.*; import java.net.*; public class URLPingTask extends TimerTask { public interface URLUpdate { public void isAlive(boolean b); } URL url; URLUpdate updater; public URLPingTask(URL url) { this(url, null); } public URLPingTask(URL url, URLUpdate uu) { this.url = url; updater = uu; } public void run( ) { if (System.currentTimeMillis( ) - scheduledExecutionTime( ) > 5000) { // Let the next task do it return; } try { HttpURLConnection huc = (HttpURLConnection) url.openConnection( ); huc.setConnectTimeout(1000); huc.setReadTimeout(1000); int code = huc.getResponseCode( ); if (updater != null) updater.isAlive(true); } catch (Exception e) { if (updater != null) updater.isAlive(false); } } }
The run()
method periodically
contacts the given URL and then updates the status watcher depending
on whether or not reading the URL was successful. Note that if more
than five seconds have elapsed since the last time the task runs, the
task skips itself.
The program that sets up the task looks like this:
package javathreads.examples.ch11.example1; import java.awt.*; import java.awt.event.*; import java.net.*; import javax.swing.*; import java.util.Timer; public class URLMonitorPanel extends JPanel implements URLPingTask.URLUpdate { Timer timer; URL url; URLPingTask task; JPanel status; JButton startButton, stopButton; public URLMonitorPanel(String url, Timer t) throws MalformedURLException { setLayout(new BorderLayout( )); timer = t; this.url = new URL(url); add(new JLabel(url), BorderLayout.CENTER); JPanel temp = new JPanel( ); status = new JPanel( ); status.setSize(20, 20); temp.add(status); startButton = new JButton("Start"); startButton.setEnabled(false); startButton.addActionListener(new ActionListener( ) { public void actionPerformed(ActionEvent ae) { makeTask( ); startButton.setEnabled(false); stopButton.setEnabled(true); } }); stopButton = new JButton("Stop"); stopButton.setEnabled(true); stopButton.addActionListener(new ActionListener( ) { public void actionPerformed(ActionEvent ae) { task.cancel( ); startButton.setEnabled(true); stopButton.setEnabled(false); } }); temp.add(startButton); temp.add(stopButton); add(temp, BorderLayout.EAST); makeTask( ); } private void makeTask( ) { task = new URLPingTask(url, this); timer.schedule(task, 0L, 5000L); } public void isAlive(final boolean b) { SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { status.setBackground(b ? Color.GREEN : Color.RED); status.repaint( ); } }); } public static void main(String[] args) throws Exception { JFrame frame = new JFrame("URL Monitor"); Container c = frame.getContentPane( ); c.setLayout(new BoxLayout(c, BoxLayout.Y_AXIS)); Timer t = new Timer( ); for (int i = 0; i < args.length; i++) { c.add(new URLMonitorPanel(args[i], t)); } frame.addWindowListener(new WindowAdapter( ) { public void windowClosing(WindowEvent evt) { System.exit(0); } }); frame.pack( ); frame.show( ); } }
Each individual panel monitors a single URL. Note that the
isAlive()
method runs from the
timer thread, so its invocation of Swing methods is placed within a
call to the invokeLater()
method.
Also note that since a task cannot be reused, the actionPerformed()
method associated with the
Start button must set up a new task.
This application points out the basic shortcomings of the
Timer
class. We’ve set it up so
that all the panels share a single instance of the timer, which means
a single thread. Although our task uses timeouts to talk to the web
server, it’s conceivable that a single execution of the run()
method of the task could take almost
two seconds (though it’s more likely to take only one second if the
site is down). If you monitor 10 sites and your ISP
goes down, the single timer thread ends
up with a backlog of tasks. That’s the reason we put logic into the
run()
method of the task to check
to see whether it missed its scheduled execution time.
The alternative is to create a new timer for each panel. In that case, we don’t have to worry about a backlog of tasks. The downside is that we now have one thread for every site we’re monitoring. That’s not a big deal unless we’re monitoring thousands of sites, but it’s not optimal either. We’ll revisit this later in this chapter.
As we’ve discussed, Swing objects cannot be accessed from arbitrary threads—which includes
the threads from the Timer
class (and
the threads in the thread pool of the ScheduledThreadPoolExecutor
class that we discuss later in this chapter). We know
that we can use the invokeLater()
and
invokeAndWait()
methods of the
SwingUtilities
class to overcome
this, but Java also provides a Timer
class just for Swing objects. The javax.swing.Timer
class provides the ability
to execute actions at a particular time, and those actions are invoked
on the event-dispatching thread.
Here is the interface to the javax.swing.Timer
class:
public class Timer { public Timer(int delay, ActionListener listener); public void addActionListener(ActionListener listener); public void removeActionListener(ActionListener listener); public ActionListener[] getActionListeners( ); public EventListener[] getListeners(Class listenerType); public static void setLogTimers(boolean flag); public static boolean getLogTimers( ); public void setDelay(int delay); public int getDelay( ) public void setInitialDelay(int initialDelay); public int getInitialDelay( ); public void setRepeats(boolean flag); public boolean isRepeats( ); public void setCoalesce(boolean flag); public boolean isCoalesce( ); public void start( ); public boolean isRunning( ); public void stop( ); public void restart( ); }
This class is not really a generic scheduler. In fact, even though
multiple callbacks (event listeners) can be attached to the timer, it
has only one schedule: all the listeners use the schedule defined by the
Timer
class itself (rather than the
schedule defined by particular tasks). Tasks that require a different
schedule need a different instance of the Swing timer. Most of the
methods provided by this class are used to configure the schedule and
control the timer.
Unlike the java.util.Timer
class, this Timer
class uses the
ActionListener
interface. This provides an interface that Swing
developers are accustomed to: all Swing objects use event listeners to
execute callbacks. When a scheduled time is reached, it is treated as
any other event (such as a button press): the registered action
listeners are called.
The constructor to the class takes two parameters. The first is the delay in milliseconds. This value is used by the timer as both the initial time to wait to fire the first action listener and the time to wait between repeated firings of the action listeners. The second parameter is an action listener to fire. Both of these parameters can be modified at a later time.
The addActionListener()
and removeActionListener()
methods are used to add listeners to and remove listeners from the
timer. The getActionListeners()
method is used to retrieve the listeners that have been registered to
the timer. The getListeners()
method
provides the added qualification of the event listener type. This allows
the developer to get specific types of listeners that are registered to
the timer. In most cases, this is probably not very useful, as the
limitation of the timer as a generic scheduler also limits the number of
action listeners registered to each timer.
The getDelay()
and setDelay()
methods are used to retrieve and modify the time between
repeated events (which by default is set in the constructor). This
allows it to be different from the initial delay time. That delay time
is handled by the getInitialDelay()
and setInitialDelay()
methods.
The isRepeats()
and setRepeats()
methods are used to control whether events are repeated.
By default, the timer repeats events, as this Timer
class was originally designed for tasks
such as a blinking cursor. The isCoalesce(
)
and setCoalesce()
methods are used to handle repeated methods that are
backlogged. For example, if a method is to be called once every second,
and three seconds have elapsed, then the listener may have to be called
three times. If the coalesce
flag is
set, the listener is called only once. This is important for tasks such
as blinking the cursor. If the timer has already missed two blinks,
blinking three times very fast does not fix the problem; it is better to
just skip the missed blinks.
The getLogTimers()
and setLogTimers()
methods are used to control debugging of the timer. If
debugging is activated, messages are sent to standard output to report
the actions of the timer.
Finally, the timer must be activated upon completion of the
registration of the listeners (and, possibly, adjusting the initial
delay and repeat times). This is accomplished by the start()
method. The timer can later be terminated by calling the
stop()
method. The restart()
method resets the timer: the timer then waits until its
initial delay time period has elapsed, at which point it starts calling
its listeners. The isRunning()
method is used to determine whether the timer has been
started.
We can use the javax.swing.Timer
class in our typing
program. Previously, our animated canvas set up a thread to handle the
animation; this thread periodically told the animation canvas to
repaint itself. Now, we’ll use a timer.
package javathreads.examples.ch11.example2; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.concurrent.*; import java.util.concurrent.locks.*; import javathreads.examples.ch11.*; public class AnimatedCharacterDisplayCanvas extends CharacterDisplayCanvas implements ActionListener, CharacterListener { private int curX; private Timer timer; public AnimatedCharacterDisplayCanvas(CharacterSource cs) { super(cs); timer = new Timer(100, this); } public synchronized void newCharacter(CharacterEvent ce) { curX = 0; tmpChar[0] = (char) ce.character; repaint( ); } public synchronized void paintComponent(Graphics gc) { if (tmpChar[0] == 0) return; Dimension d = getSize( ); int charWidth = fm.charWidth(tmpChar[0]); gc.clearRect(0, 0, d.width, d.height); gc.drawChars(tmpChar, 0, 1, curX++, fontHeight); if (curX > d.width - charWidth) curX = 0; } public void actionPerformed(ActionEvent ae) { repaint( ); } public void setDone(boolean b) { if (!b) timer.start( ); else timer.stop( ); } }
Note that this implementation is much simpler than our previous
implementations. Previously, we set up a thread in the setDone()
method; now, we simply call the
timer start()
method.
Using the timer has also allowed us to simplify the locking
around the calls to the repaint()
method. Knowing when the animation should run used to require a
wait-and-notify mechanism (or condition variable). Now we just defer
that to the timer. The Timer
class
itself has the waiting logic within it: operationally, we haven’t
saved anything. But in terms of development, using a timer has saved
us some effort. This is a clear example of why using higher-level thread
constructs makes things simpler for the developer.
J2SE 5.0 introduced the ScheduledThreadPoolExecutor
class, which solves many problems of the Timer
class. In many regards, the Timer
class can be considered obsolete because
of the ScheduledThreadPoolExecutor
class. Why is this class needed? Let’s examine some of the problems with
the Timer
class.
First, the Timer
class starts
only one thread. While it is more efficient than creating a thread per
task, it is not an optimal solution. The optimal solution may be to use
a number of threads between one thread for all tasks and one thread per
task. In effect, the best solution is to place the tasks in a pool of
threads. The number of threads in the pool should be assignable during
construction to allow the program to determine the optimal number of
threads in the pool.
Second, the TimerTask
class is
not necessary. It is used to attach methods to the task itself,
providing the ability to cancel the task and to determine the last
scheduled time. This is not necessary: it is possible for the timer
itself to maintain this information. It also restricts what can be
considered a task. Classes used with the Timer
class must extend the TimerTask
class; this is not possible if the
class already inherits from another class. It is much more flexible to
allow any Runnable
object to be used
as the task to be executed.
Finally, relying upon the run()
method is too restrictive for tasks. While it is possible to pass
parameters to the task—by using parameters in the constructor of the
task—there is no way to get any results or exceptions. The run()
method has no return variable, nor can
it throw any type of exceptions other than runtime exceptions (and even
if it could, the timer thread wouldn’t know how to deal with it).
The ScheduledThreadPoolExecutor
class solves all three of these problems. It uses a thread pool
(actually, it inherits from the thread pool class) and allows the
developer to specify the size of the pool. It stores tasks as Runnable
objects, allowing any task that can
be used by the thread object to be used by the executor. Because it can
work with objects that implement the Callable
interface, it eliminates the
restrictive behavior of relying solely on the Runnable
interface.
Here’s the interface of the ScheduledThreadPoolExecutor
class
itself:
public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor { public ScheduledThreadPoolExecutor(int corePoolSize); public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory); public ScheduledThreadPoolExecutor(int corePoolSize, RejectedExecutionHandler handler); public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory, RejectedExecutionHandler handler); public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit); public ScheduledFuture<V> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); public ScheduledFuture<V> scheduleWithFixedDelay( Runnable command, long initialDelay, long delay, TimeUnit unit); public void execute(Runnable command); public void shutdown( ); public List shutdownNow( ); public void setContinueExistingPeriodicTasksAfterShutdownPolicy( boolean value); public boolean getContinueExistingPeriodicTasksAfterShutdownPolicy( ); public void setExecuteExistingDelayedTasksAfterShutdownPolicy( boolean value); public boolean getExecuteExistingDelayedTasksAfterShutdownPolicy( ); }
The ScheduledThreadPoolExecutor
class provides four constructors to create an object. These parameters
are basically the same parameters as the thread pool constructors since
this executor inherits from the thread pool executor. Therefore, this
class is also a thread pool, meaning that some of the parameters
assigned by these constructors can also be retrieved and modified by the
methods of the ThreadPoolExecutor
class.
Note, however, that the constructors have no parameter to specify
the maximum number of threads or the type of queue the thread pool
should use. The scheduled executor always uses an unbounded queue for
its tasks, and the size of its thread pool is always fixed to the number
of core threads. The number of core threads, however, can still be
modified by calling the setCorePoolSize()
method.
The schedule()
method is used
to schedule a one-time task. You can use the ScheduledFuture
object returned by this method
to perform the usual tasks on the callable object: you can retrieve its
result (using the get()
method),
cancel it (using the cancel( )
method), or see if it has completed execution (using the isDone()
method).
The scheduleAtFixedRate()
method is used to schedule a repeated task that is not allowed to drift.
This is basically the same scheduling model as the scheduleAtFixedRate()
method of the Timer
class.
The scheduleWithFixedDelay()
method is used to schedule a repeated task where the period between the
tasks remains constant; this is useful when the delay between iterations
is to be fixed. For instance, this model is better for animation since
there is no reason to have animation cycles accumulate if the start
times drift. If one cycle of the animation runs late, there is no
advantage to running the next cycle earlier.
Table 11-2 shows
when tasks would be executed under different scheduling models of the
ScheduledThreadPoolExecutor
class. In
this example, we’re again assuming that the task is to be run every
second, executes for .1 seconds, and the system becomes bogged down for
.5 seconds between the second and third iteration. The scheduleAtFixedRate()
method runs the delayed
iteration .5 seconds late but still executes the remaining iterations
according to the original schedule (exactly the same as the java.util.Timer
class). The scheduleWithFixedDelay()
method takes into
account the execution time of the task; this is why each iteration
drifts by .1 seconds. It does not compensate for the .5-second delay, so
it drifts over time.
The execute()
and submit()
methods are used to schedule a task
to run immediately. These methods are present mainly because the
Executor
interface requires them.
Still, it may be useful for one task to add other tasks to be run in the
pool rather than execute them directly, because then the primary task
doesn’t own the thread in the pool for a huge period of time. It also
allows the thread pool to assign the subtasks to other threads in the
pool if the pool is not busy.
The shutdown()
and shutdownNow()
methods are also part of the
thread pool class. The shutdown()
method is used to shut down the executor but allows all pending tasks to
complete. The shutdownNow()
method is
used to try to cancel the tasks in the pool in addition to shutting down
the thread pool. However, this works differently from a thread pool
because of repeating tasks. Since certain tasks repeat, tasks could
technically run forever during a graceful shutdown.
To solve this, the task executor provides two policies. The
ExecuteExistingDelayedTasksAfterShutdownPolicy
is used to determine whether the tasks in the queue should be cancelled
upon graceful shutdown. The ContinueExistingPeriodicTasksAfterShutdownPolicy
is used to determine whether the repeating tasks in the queue should be
cancelled upon graceful shutdown. Therefore, setting both to false
empties the queue but allows currently
running tasks to complete. This is similar to how the Timer
class is shut down. The shutdownNow()
method cancels all the tasks and
also interrupts any task that is already executing.
With the support of thread pools, callable tasks, and fixed delay
support, you might conclude that the Timer
class is obsolete. However, the Timer
class has some advantages. First, it
provides the option to specify an absolute time. Second, the Timer
class is simpler to use: it may be
preferable if only a few tasks or repeated tasks are needed.
Here’s a modification of our URL monitor that uses a scheduled executor. Modification of the task itself means a simple change to the interface it implements:
package javathreads.examples.ch11.example3; ... public class URLPingTask implements Runnable { ... }
Our Swing component has just a few changes:
package javathreads.examples.ch11.example3; ... import java.util.concurrent.*; public class URLMonitorPanel extends JPanel implements URLPingTask.URLUpdate { ScheduledThreadPoolExecutor executor; ScheduledFuture future; ... public URLMonitorPanel(String url, ScheduledThreadPoolExecutor se) throws MalformedURLException { executor = se; ... stopButton.addActionListener(new ActionListener( ) { public void actionPerformed(ActionEvent ae) { future.cancel(true); startButton.setEnabled(true); stopButton.setEnabled(false); } }); ... } private void makeTask( ) { task = new URLPingTask(url, this); future = executor.scheduleAtFixedRate( task, 0L, 5L, TimeUnit.SECONDS); } public static void main(String[] args) throws Exception { ... ScheduledThreadPoolExecutor se = new ScheduledThreadPoolExecutor( (args.length + 1) / 2); for (int i = 0; i < args.length; i++) { c.add(new URLMonitorPanel(args[0], se)); } ... } }
The main enhancement that this change has bought us is the ability to specify a number of threads for the executor. We’ve chosen half as many threads as the machines we’re monitoring: in between the number of suboptimal choices we had previously. In this case, it would have been even more ideal for the task executor to be more flexible in its thread use.
The other case when using a scheduled executor makes sense is when you want to use
the callable interface so that you can later check the status of the
task. This is logical equivalent to using the join()
method to tell if a thread is
done.
We’ll extend our example slightly to see how this works. Let’s suppose we want our URL monitor to have a license; without a license, it runs in a demo mode for two minutes. In the absence of a valid license, we can set up a callable task that runs after a delay of two minutes. After that task has run, we know that the license period has expired.
We’ll have to poll the license task periodically to see whether it has finished. Normally, we don’t like polling because of its inefficiencies, but in this case, we have a perfect time to do it: because the status thread runs every five seconds, it can poll the license task without wasting much CPU time at all. Since in this case we don’t have to unnecessarily wake up a polling thread, we can afford the simple method call to handle the poll.
First, we need a simple task.
package javathreads.examples.ch11.example4; class TimeoutTask implements Callable { public Integer call( ) throws IOException { return new Integer(0); } }
As required, we’ve implemented the Callable
interface. In this simple example,
we don’t actually care about the return value: if the task has run,
the license has expired. In a more complicated case, the license task
might check with a license server and return a more interesting
result. Checking with the license server might create an IOException
, which is why we’ve declared
that this task throws that exception.
Now we must add this to our monitor:
package javathreads.examples.ch11.example4; public class URLMonitorPanel extends JPanel implements URLPingTask.URLUpdate { static Future<Integer> futureTaskResult; static volatile boolean done = false; ... private void checkLicense( ) { if (done) return; try { Integer I = futureTaskResult.get(0L, TimeUnit.MILLISECONDS); // If we got a result, we know that the license has expired JOptionPane.showMessageDialog(null, "Evaluation time period has expired", "Expired", JOptionPane.INFORMATION_MESSAGE); done = true; } catch (TimeoutException te) { // Task hasn't run; just coninue } catch (InterruptedException ie) { // Task was externally interrupted } catch (ExecutionException ee) { // Task threw IOException, which can be obtained like IOException ioe = (IOException) ee.getCause( ); // Clean up after the exception } } public void isAlive(final boolean b) { try { SwingUtilities.invokeAndWait(new Runnable( ) { public void run( ) { checkLicense( ); if (done) { future.cancel(true); startButton.setEnabled(false); stopButton.setEnabled(false); return; } status.setBackground(b ? Color.GREEN : Color.RED); status.repaint( ); } }); } catch (Exception e) {} } public static void main(String[] args) throws Exception { ... TimeoutTask tt = new TimeoutTask( ); futureTaskResult = se.schedule(tt, 120, TimeUnit.SECONDS); ... } }
The checkLicense()
method is
called every time status is reported; it polls the timeout task. When
the poll succeeds, the checkLicense()
method sets a done
flag so that other panels know that the
license has expired (the done
flag
is static and shared among all panels). Alternately, we could let each
panel poll the futureTaskResult
object itself.
If you look carefully, you’ll notice that there’s no
synchronization for the checkLicense()
method and that it appears
that the option pane might get displayed twice if two panels invoke
that method at the same time. However, that’s not possible because the
checkLicense()
method is called via
the invokeAndWait()
method. That
blocks the event-dispatching thread so we are already assured that
only one thread at a time is executing the checkLicense()
method.
In this chapter, we’ve looked at various ways in which tasks may
be scheduled in the future. The simplest way to do this is to use the
java.util.Timer
class, which can run
instances of a special class (the TimerTask
class) at a point in the future,
repeating the task if necessary. Each instance of a timer is a single
thread; that thread can handle multiple tasks but long-running tasks may
need their own thread (and consequently their own timer).
The javax.swing.Timer
class is
functionally similar, except that it ensures that tasks are run on the
event-dispatching thread so that they may safely access Swing
components. However, the javax.swing.Timer
class has a fixed time
schedule for all the tasks it runs; tasks that have different scheduling
needs require different instances of the timer.
Finally, the ScheduledThreadPoolExecutor
class provides a
more flexible (but more complex) interface to task scheduling. Because
it uses a thread pool, it can be more efficient when running a lot of
tasks simultaneously. It also allows you to poll for task status or to
use generic Runnable
objects as your
task.
The key benefit of task executors and timers is that they free you from having to worry about thread-related programming for your tasks: you simply feed the task to the timer or executor and let it worry about the necessary thread controls. This makes the code that you write that much simpler.
Here are the class names and Ant targets for the examples in this chapter:
Description | Main Java class | Ant target |
---|---|---|
URL Monitor with | | ch11-ex1 |
Type Tester with Timer animation | | ch11-ex2 |
URL Monitor with scheduled executor | | ch11-ex3 |
URL Monitor with timeout | | ch11-ex4 |
The ant property to specify the URL is:
<property name="hostlist" value="http://www.ora.com/"/>
Unfortunately, Ant offers no way to specify multiple hostnames. If you want to try a URL monitor with more than one URL, you must execute the class directly.