站内搜索: 请输入搜索关键词
当前页面: 图书首页 > Java Threads, Third Edition

11.4 The ScheduledThreadPoolExecutor Class - Java Threads, Third Edition

Previous Section  < Day Day Up >  Next Section

11.4 The ScheduledThreadPoolExecutor Class

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梑y using parameters in the constructor of the task梩here 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.

Table 11-2. Execution time of java.util.Timer tasks
 

Execution start time

Method

Iteration 1

Iteration 2

Iteration 3

Iteration 4

Iteration 5

scheduleAtFixedRate()

1 seconds

2 seconds

3.5 seconds

4 seconds

5 seconds

scheduleWithFixedDelay()

1 seconds

2.1 seconds

3.7 seconds

4.8 seconds

5.9 seconds


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.

11.4.1 Using the ScheduledThreadPoolExecutor Class

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.

11.4.2 Using the Future Interface

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.

    Previous Section  < Day Day Up >  Next Section