 | Level: Introductory Eric Allen (eallen@cs.rice.edu), PhD Candidate, Rice University
01 Aug 2001 In multithreaded code, it is often common to use a single, master thread that drives the actions the other threads take. This master thread may send messages, often by placing them on a queue, that are then processed by the other threads. But if the master thread throws an exception, the remaining threads may continue to run, awaiting more input to the queue, causing the program to freeze. In this installment of Diagnosing Java Code, full-time Java developer and part-time exterminator Eric Allen discusses detecting, fixing, and avoiding this bug pattern. Share your thoughts on this article with the author and other readers in the discussion forum by clicking Discuss at the top or bottom of the article. Writing code with multiple threads can be quite advantageous to programmers. Multiple threads can make the programming (and the program) go much faster, and the code can use resources much more effectively. However, as with most things in life, there are drawbacks. Because multithreaded code is inherently non-deterministic, the potential for errors is much greater. What's more, the errors that do occur will be much harder to replicate and, therefore, tougher to pin down. The Orphaned Thread pattern
The Java programming language provides abundant support for multithreaded code, including one feature that can be especially useful: the ability to throw an exception in one thread without affecting the others. But this feature can cause many hard-to-track bugs.  |
Fast-track to the code
Listing 1.
An example program in which threads communicate with each other often.
Listing 2.
Demonstrates how to catch exceptions and notify dependent threads of the problem before exiting.
|
|
In cases where it makes sense to recover from a crash in one of the threads, this ability can allow for an added level of robustness to a program. It can, however, also make it difficult to determine when one of these threads has thrown an exception. Because the remaining threads will continue to run, the program may exhibit the signs of an unresponsive or frozen program. This is particularly true in a program where the threads often communicate with each other. Consider the example shown in Listing 1 in which a pair of threads communicate via a producer-consumer model. Listing 1. A simple, multithreaded consumer-producer program
public class Server extends Thread {
Client client;
int counter;
public Server(Client _client) {
this.client = _client;
this.counter = 0;
}
public void run() {
while (counter < 10) {
this.client.queue.addElement(new Integer(counter));
counter++;
}
throw new RuntimeException("counter >= 10");
}
public static void main(String[] args) {
Client c = new Client();
Server s = new Server(c);
c.start();
s.start();
}
}
class Client extends Thread {
Vector queue;
public Client() {
this.queue = new Vector();
}
public void run() {
while (true) {
if (! (queue.size() == 0)) {
processNextElement();
}
}
}
private void processNextElement() {
Object next = queue.elementAt(0);
queue.removeElementAt(0);
System.out.println(next);
}
}
|
In a case such as this, the second thread is entirely dependent upon the first to receive any data with which to compute. It is, therefore, inevitable that if the first thread crashes (and, in this sample, it is guaranteed to do so), the second thread will wait for further input that will never come. Now you know why I call this pattern of bug the orphaned thread pattern.
The symptoms
The most common symptom of this bug pattern is the one I mentioned earlier -- namely, that the program will appear to freeze. Other symptoms may include stack traces printed to standard error and standard out with the program actually halting.
Cures and preventions
Once a bug of this pattern has been diagnosed, the obvious cure is to find and fix the underlying error in the crashing thread. But prevention is more difficult. It goes without saying that if you can get away with using a single-thread design, you'll eliminate many headaches. Chances are, though, that if a multithreaded design was considered in the first place, it was necessary for the performance requirements of the program. One way to aid in the diagnosis of such crashes is to catch the exceptions thrown in the various threads and notify the dependent threads of the problem before exiting. This is what I've done in Listing 2. Listing 2. Example in which the client thread is notified of an error
import java.util.Vector;
public class Server2 extends Thread {
Client2 client;
int counter;
public Server2(Client2 _client) {
this.client = _client;
this.counter = 0;
}
public void run() {
try {
while (counter < 10) {
this.client.queue.addElement(new Integer(counter));
counter++;
}
throw new RuntimeException("counter >= 10");
}
catch (Exception e) {
this.client.interruptFlag = true;
throw new RuntimeException(e.toString());
}
}
public static void main(String[] args) {
Client2 c = new Client2();
Server2 s = new Server2(c);
c.start();
s.start();
}
}
class Client2 extends Thread {
Vector queue;
boolean interruptFlag;
public Client2() {
this.queue = new Vector();
this.interruptFlag = false;
}
public void run() {
while (! interruptFlag) {
if (! (queue.size() == 0)) {
processNextElement();
}
}
// Processes whatever elements remain on the queue before exiting.
while (! (queue.size() == 0)) {
processNextElement();
}
System.out.flush();
}
private void processNextElement() {
Object next = queue.elementAt(0);
queue.removeElementAt(0);
System.out.println(next);
}
}
|
Other options for handling the thrown exception would be to call System.exit. This option makes sense when the crash occurs in the program's main thread and the other threads don't manage any critical resources. In other cases, though, it can be dangerous. For example, consider an example in which one of the other threads was managing an open file. If this is the case, simply exiting the program could cause resource leakage. Even in the simple example above, calling System.exit in the server thread would cause the client to exit without processing any remaining elements on its queue. In fact, problems such as these were what spurred Sun into deprecating the stop method on threads. Since stopping a thread without its consent may leave resources in an inconsistent state, the stop method violated the security model of the language. For more on Sun's reasons for deprecating, see Resources.
Wrapup
Here is this week's bug pattern in a nutshell:
- Pattern: Orphaned Thread
- Symptoms: A multithreaded program that locks up, with or without the printing of stack traces to standard error.
- Cause: Various program threads are stuck waiting for input from a thread that exited after an uncaught exception was thrown.
- Cures and preventions: Put exception-handling code in the main thread to notify the dependent threads when a crash is imminent.
Resources
About the author  | |  |
Eric Allen has a bachelor's degree in computer science and mathematics from Cornell University and is a PhD candidate in the Java programming languages team at Rice University. Before returning to Rice to work full time on his degree, Eric was the lead Java software developer at Cycorp, Inc. He has also moderated the Java Beginner discussion forum at JavaWorld. His research concerns the development of semantic models and static analysis tools for the Java language, both at the source and bytecode levels. Eric has also helped in the development of Rice's compiler for the NextGen programming language, an extension of the Java language with generic run-time types. Contact Eric at eallen@cs.rice.edu.
|
Rate this page
|  |