HANDLING THOSE PESKY
INTERRUPTEDEXCEPTIONS
If you have done any thread-related programming in the Java
programming language, you have have been forced to deal with
InterruptedExceptions. These exceptions appear in the throws clause
of Thread.sleep(), Thread.join(), and Object.wait(). An
InterruptedException allows code on another thread to interrupt
your thread if, for example, your thread takes too long to process.
Many programmers rarely use interruption, and find these exceptions
annoying. But even if your code never interrupts other threads,
there are two reasons you should care about interruption.
o InterruptedException is a checked exception, which means that
your code must catch or propagate the exception, even if you
never expect it to happen.
o In the Java environment, you cannot typically rely on
controlling the entire process in which your code runs. This
is good, because it allows for the use of things like mobile
agents, container architectures, applets, and RMI. However, it
also means that even if you never call Thread.interrupt() on
one of your threads, somebody else probably will.
This tip compares three different strategies for handling
InterruptedExceptions: propagate them, ignore them, or defer them.
The first strategy is to propagate the exception back to whoever
calls your code. Here's an example:
file://throughout this example error checking omitted for brevity
interface Task {
public void run() throws InterruptedException;
}
class PropagatingTask implements Task {
public void run() throws InterruptedException {
Thread.sleep(1000);
System.out.println("PropagatingTask completed");
}
}
public class TaskClient implements Runnable {
public static final int taskCount = 1000;
Task[] tasks;
public void doMain(Task[] tasks) {
this.tasks = tasks;
Thread worker = new Thread(this);
worker.start();
try {
System.in.read();
}
catch (java.io.IOException ioe) {
System.out.println("I/O exception on input");
}
System.out.println("==========================Shutting down");
worker.interrupt();
try {
worker.join();
}
catch (InterruptedException ie) {
System.out.println("Unexpected interruption on main thread");
}
}
public void run() {
try {
for (int n=0; n<taskCount; n++) {
tasks[n].run();
if (Thread.interrupted()) {
System.out.println("Interrupted state detected by client");
throw new InterruptedException();
}
}
}
catch (InterruptedException ie) {
System.out.println("Interruption caught by client");
}
}
public static void main(String[] args) {
try {
Class cls = Class.forName(args[0]);
Task[] tasks = new Task[taskCount];
for (int n=0; n<taskCount; n++) {
tasks[n] = (Task) cls.newInstance();
}
new TaskClient().doMain(tasks);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Try running TaskClient by entering the following on the command
line:
java TaskClient PropagatingTask
TaskClient expects a single command line argument which names an
implementation of the Task interface, in this case,
Propagatingtask. TaskClient then creates 1000 tasks, and runs them
on a background thread. Each PropagatingTask sleeps for one second
and prints "PropagatingTask completed."
You will probably get bored and want to interrupt the thread
before all 1000 tasks complete. The main thread allows this by
reading from System.in. Try this by pressing the Enter key. When
you do this, the main thread calls interrupt() on the worker thread.
It then calls join(); this allows the worker thread to complete
before the main thread exits. Your output should end like this:
PropagatingTask completed
==========================Shutting down
Interruption caught by client
Notice that no tasks complete after the interruption. This means
that all the tasks not yet started never get the chance to start.
It also means that the one task in progress is rudely interrupted
in the middle of processing. Both of these behaviors are a
consequence of the PropagatingTask allowing the
InterruptedException to propagate all the way back to the caller.
The PropagatingTask implementation is the simplest way to deal with
InterruptedExceptions, and it has the advantage of allowing you to
interrupt the thread so that no new tasks begin. However, this
approach has two disadvantages: (1) the caller is forced to handle
InterruptedExceptions, and (2) the task that was in progress is
forcibly stopped; this might be unacceptable if the task left data
in some invalid state. Here is another approach, one that addresses
the first problem:
class IgnoringTask implements Task {
public void run() {
long now = System.currentTimeMillis();
long end = now + 1000;
while (now < end) {
try {
Thread.sleep(end-now);
}
catch (InterruptedException ie) {
System.out.println("IgnoringTask ignoring interruption");
}
now = System.currentTimeMillis();
}
System.out.println("IgnoringTask completed");
}
}
IgnoringTask uses System.currentTimeMillis() to keep track of
elapsed time, and if an InterruptedException is thrown, it catches
the exception and goes back to finish its work. Because the
InterruptedException is not thrown from the run() method, it is not
declared as a checked exception, and clients do not have to handle
it. Try running IgnoringTask by entering the following on the
command line:
java TaskClient IgnoringTask
If you press Enter to interrupt the thread, you will see this
output:
==========================Shutting down
IgnoringTask ignoring interruption
IgnoringTask completed
IgnoringTask completed
etc.
Notice that "IgnoringTask completed" continues to be printed. As
you can see, an IgnoringTask cannot be interrupted midstream. This
is appropriate in most situations. Unfortunately, an IgnoringTask
also prevents the thread from being interrupted at all. Even after
you try to interrupt the thread, new tasks will continue to run.
You have made your thread permanently uninterruptible, and other
programmers who use your code are not likely to be happy.
What you need is some way to guarantee that tasks already in
progress will finish, but still provide some way to interrupt the
thread. The DeferringTask class provides a solution:
class DeferringTask implements Task {
public void run() {
long now = System.currentTimeMillis();
long end = now + 1000;
boolean wasInterrupted = false;
while (now < end) {
try {
Thread.sleep(end-now);
}
catch (InterruptedException ie) {
System.out.println("DeferringTask deferring interruption");
wasInterrupted = true;
}
now = System.currentTimeMillis();
}
System.out.println("DeferringTask completed");
if (wasInterrupted) {
Thread.currentThread().interrupt();
}
}
}
DeferringTask is almost exactly the same as IgnoringTask, with
one crucial difference. DeferringTask remembers that it was
interrupted by setting the boolean flag wasInterrupted. When the
task completes, DeferringTask calls interrupt() to reset the
interrupt flag. Because interrupt() sets a flag instead of
throwing an InterruptedException, the client does not have to
catch an InterruptedException. Instead, it can check the setting
of the interrupt flag by calling Thread.interrupted(), which is
what TaskClient.run() does. Try running DeferringTask as follows:
java TaskClient DeferringTask
When you trigger the interrupt() by pressing enter, you should see
output that ends like this:
==========================Shutting down
DeferringTask deferring interruption
DeferringTask completed
Interrupted state detected by client
Interruption caught by client
Notice that a single DeferringTask completes after interruption.
This was the one task in progress. However no new tasks begin
because DeferringTask resets the interrupt flag, and that stops
the thread.
No single interruption strategy is appropriate for all situations.
Here is a summary of the three strategies in this tip:
----------------------------------------------------------------
| Strategy | Client must | Tasks forcibly | No new tasks begin |
| | catch IE? | stopped | after interrupt? |
|----------------------------------------------------------------|
| propagate| yes | yes | yes |
| ignore | no | no | no |
| defer | no* | no | yes* |
|----------------------------------------------------------------|
| * for defer to work correctly, caller must check for |
| interruption |
----------------------------------------------------------------
For a more in-depth look at interruption, refer to Chapter 9 of
Multithreaded Programming with Java Technology, by Bil Lewis and
Daniel J. Berg (for information about this book, see
http://www.sun.com/books/catalog/lewis3/).
|