Bug Patterns In Java - The Orphaned Thread - Online Article

Overview

When your multithread program freezes and stack traces don't print to standard error, there's a chance you've discovered the hard-to-track Orphaned Thread bug pattern. We discuss the obvious fix and detail ways to detect and prevent this bug.

Writing code with 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 nondeterministic, the potential for errors is much greater. What's more, the errors that do occur will be much harder to reproduce, and therefore tougher to pin down.

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.

Tip

Multithreaded code is inherently nondeterministic, so it can potentially introduce errors that will be much harder to reproduce and identify.

In cases where it makes sense to recover from a crash in one of the threads, this ability can add a level of robustness to a program. It can, however, make it difficult to determine which thread 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 communicate with each other often. I call this the Orphaned Thread bug pattern.

Here is this bug pattern in a nutshell:

  • Pattern: Orphaned Thread.
  • Symptoms: A multithreaded program locks up with or without printing 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 of the exceptional condition. Alternatively, put a handler into the thread that exited so that it passes information to its clients instead.

About This Bug Pattern

The fact that one thread can throw an exception without affecting other threads can make it difficult to determine which thread has actually thrown the exception.

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 for the data it needs to compute. If the first thread crashes, the second thread will inevitably wait for further input that will never come, essentially freezing the program. The second thread has been abandoned, which is 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. If you see stack traces printed to standard error and to standard out, that's another symptom.

Such stack traces are especially common with GUI applications on Unix-based systems in which the applications are launched from a terminal window. When a GUI application spills a stack trace to the terminal while freezing, strongly suspect an Orphaned Thread.

The Cause

Various program threads are stuck waiting for input from a thread that exited after an uncaught exception was thrown.

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. Prevention, however, is more difficult. If you can get away with using a single-threaded design, you'll eliminate many headaches. Chances are, however, that if you settled on a multithreaded design in the first place, there was a good reason for that design choice.

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.

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);
}
}

If the exceptional circumstance indicates a bug in the program, another option for handling the thrown exception would be to put up a short message notifying the user of the problem and then call System.exit. This option is preferable to allowing the program to freeze, since a user can never really be sure that a program is frozen. He may decide that if he waits just a little longer, it'll come back. But an explicit message indicating a catastrophic error is unmistakable.

This option particularly 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, what if one of the other threads were accessing a shared database? In that case, simply exiting the program could prevent that thread from freeing a lock on the database. 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 to deprecate the stop() method on threads. That method allowed the termination of a thread without its consent. Because it may leave resources in an inconsistent state, the stop() method violated the security model of the language.

 
Tip

Sun deprecated the stop() method on threads because terminating a thread without its consent leaves resources in an inconsistent state, thereby violating Java's security model.

In addition to freeing shared resources, we'd also like to offer the user the opportunity to save unsaved data and perhaps file a bug report (as is common in many newer applications, especially on Windows XP). Ideally, tasks like saving files, filing bug reports, and unlocking shared resources could all be done in the exception handling code before calling System.exit.

But remember, we are assuming that there is a bug in the program—the clean up code might trip over the same bug! That would result in a recursive cascade of error messages to the user, which is clearly not what we want. On the other hand, perhaps most users would prefer to risk this cascade of messages in exchange for the chance to save their data.

Orphaned Threads and GUIs

In the context of GUI programming, it is quite easy to introduce orphaned threads. Often, GUI programs will spawn threads in response to various events, each time locking the application until the spawned thread returns. But if the spawned thread throws an unchecked exception, you've got an orphaned thread.

In our first attempt to integrate support for JUnit into DrJava, we added a Test button to the toolbar. When pressed on a valid subclass of junit.framework.TestCase, this button would spawn a separate thread to invoke JUnit on the corresponding class file.

As with the interactions pane, DrJava would not require the user to explicitly put a class on the system classpath before trying to test it. Instead, DrJava would check the package name of the test class and automatically add the implied root directory of the source packages to the class path passed to JUnit. Provided that the classfiles were kept in the same directories as the corresponding source files (as is done by default in DrJava), this would work fine.

Also, if the user modified the file, DrJava would prompt him to save and compile before calling JUnit. But if the user had compiled the classfile for a test class to a package directory hierarchy distinct from that of the source file, the directory hierarchy for the classfile would not be on his system classpath. If the user opened the corresponding source file and immediately called Test on it, JUnit would be unable to locate the corresponding class file. It would throw a ClassNotFoundException.

But this scenario was overlooked, which meant that we didn't code in logic to handle these ClassNotFoundExceptions. So they propagated to the top level of the spawned thread, resulting in its termination.

Meanwhile, the application would lock until the spawned thread returned a report of the result of running JUnit on the test class—which, of course, would never happen. The result? Every user's worst nightmare: a frozen application.

Once this problem was identified, the solution was easy: catch the ClassNotFoundException thrown from JUnit and generate an appropriate report of the result. Of course, a new unit test was put into place to make sure that this particular bug never occurred again.

Unit Tests and Multithreading

Because DrJava makes heavy use of multithreading, we have encountered Orphaned Threads, along with other multithreading bugs, on many occasions. But, luckily, few of these bugs make it past our unit tests. One main reason that the unit tests save us so often from multithreading problems is that the tests interact with the application much more quickly than any human user could. This ultrafast interaction tends to cause all of the program threads to interact in unexpected ways, revealing bugs that a user wouldn't normally encounter. (Of course, on the rare occasion that a user did happen to trip up on such a bug, it would be extraordinarily difficult to diagnose, or even repeat.) For this reason, a strong suite of unit tests over multithreaded code is essential, even more so than in other contexts.

As the earlier examples demonstrate, Orphaned Threads can have devastating symptoms. And in the context of a multithreaded application, they aren't hard to generate.

Tip

Use the sharp knife of multithreading with caution. Although it can deliver a more efficient and understandable program, it is an easy way to break an invariant expected to hold in one thread.

What We've Learned

In this article on the Orphaned Thread bug pattern we've learned the following:

  • The most common symptom of this pattern is that the program will appear to freeze. Stack traces printed to standard error and standard out are another clue.
  • The obvious cure is to find and fix the underlying error in the crashing thread. Prevention is a more difficult issue.
  • One way to diagnose this problem is to catch the exceptions thrown in the various threads and notify the dependent threads of the problem before exiting.
  • An option for handling the thrown exception would be a message notifying the user of the problem and a call to System.exit. This option doesn't allow the program to freeze and it serves as an unmistakable marker for the user. This option works when the crash occurs in the program's main thread and other threads don't manage critical resources—but it can be dangerous in other cases.
  • The ideal situation is one in which tasks like saving files, filing bug reports, and unlocking shared resources could all be done in the exception handling code before calling System.exit.
  • Multithreaded code is inherently nondeterministic, so its potential for errors is greater and those errors will be much harder to reproduce and identify.
  • Unit tests are a great way to discover bugs in multithreaded code, as the interactions between the threads tend to occur differently than they do when a human user interacts with the program.
  • Java provides abundant support for multithreaded code, including the useful ability to throw an exception in one thread without affecting the others—but this feature can cause hard-to-track bugs.

About the Author:

No further information.




Comments

No comment yet. Be the first to post a comment.