Multi-Threaded Exception Handling in Java

 

Multithreaded Exception Handling in Java
Joe De Russo III and Peter Haggar

Multithreaded programming has been with us for many years and is considered to be a feature that many robust applications utilize. Exception handling is another feature of many languages considered to be necessary for proper and complete error handling. Each of these technologies stands very well on their own. In fact, you cannot pick up a book about Java programming without finding a chapter devoted to each of these topics. Many of these books do a good job defining and describing how to use them properly in your programs. What is missing is the information on how to use these two technologies together effectively. After all, how effective is writing a multithreaded Java program if it is incapable of properly handling exceptions occurring on secondary threads? We will present a solution for effectively dealing with this problem.

To solve this problem, we introduce two new classes and two new interfaces to be used when writing multithreaded Java programs. These classes are small, easy to use and understand, and effectively enable you to handle exceptions occurring on secondary threads.

Writing robust code implies many things. One of them is the proper and effective way in which your program deals with error situations. The approach you take can vary from doing nothing to handling any and all problems. The approach you choose is more than likely dictated by the type of application you are writing. For example, if you are writing an application that must run uninterrupted for many hours, days, or months at a time, you will need to effectively employ an error handling strategy that will ensure your software can run uninterrupted when errors occur. Even if you are not writing such a program, effectively dealing with errors is just good programming practice.

One of the main areas of importance for dealing with exceptions is what we call state management. This involves ensuring that when an exception occurs, the state of the object the exception occurs in remains valid such that if the code recovers from the exception, the object can be reliably used again. Doing so in single-threaded applications is challenging enough without introducing multiple threads of execution.

The core problem that must be dealt with is how to manage concurrent threads of execution when one, or many, of those threads may terminate abnormally. We need a way to be notified of any errors occurring on secondary threads, and a solution that enables us to terminate gracefully while optionally preserving object state. Java is currently being considered as a platform for deploying mission-critical applications. Multithreaded exception handling is a reality in this environment. While much documentation exists on the virtues of using threads of execution and exceptions in Java, there is little documentation on how to integrate the two. One such solution to these problems is presented here. We designed our solution to be as generic as possible. Our design goals were to provide a solution that:

• Does not tightly couple the objects and code running on secondary threads with the objects and code that need to know if an exception occurs. For example, we do not implement a simple callback interface.

• Requires a minimum amount of maintenance when the code is changed to throw additional exceptions from the secondary threads.

• Minimizes the number of try/catch blocks.

• Works even if the code to be executed on secondary threads is not owned by the developer calling it. This could occur if you are implementing a run() method that calls code you obtained from a third party, and you want to catch any exception thrown from it.

• Works for all exceptions, both checked and unchecked, that may be thrown from within a secondary thread.

Note: Throughout this article we will be using the notion of checked and unchecked exceptions.

Checked exceptions are generally related to conditions that are specific to an operation being performed, such as trying to construct an invalid URL. The compiler requires that you take action on all checked exceptions that may occur in your method in one of two ways: either by handling them yourself with a try/catch block or by advertising to your callers that your method throws this exception by listing them in that method’s throws clause. In contrast, unchecked exceptions could occur anywhere in your program, such as an out-of-memory condition. Although it may be useful to be aware of these problems in certain situations, the compiler does not require you to address unchecked exceptions.

The solution identified in this article satisfies all of the afforementioned design goals and is straightforward and generic enough to be used in production systems. All of the code presented here was compiled and run using the Sun JDK 1.1.4 on the Windows NT 4.0 Operating System.

Multithreaded
Exception Handling

We present our solution to this problem in the following manner: Appendix A (posted at Java Report Online) contains a complete multithreaded program that attempts to open separate files on two threads. Listings 1-4 examine this code, point out the problems contained in it, and offer some initial attempts at solving them. Listings 5-8 introduce two new classes and two new interfaces to provide a solution to these problems. Appendix B (see Java Report Online) contains the program in Appendix A modified with the classes and interfaces introduced in Listings 5-8 such that it correctly deals with exceptions occurring on secondary threads. Listings 9-11 examine, more closely, Appendix B and offer commentary on how it was developed to solve these problems.

The Initial Code

The code in Appendix A contains a user interface that has two buttons and two listboxes. The listboxes are used to display the files and the buttons are used to fill the first listbox on the main thread and start the secondary thread to attempt to fill the second listbox. The code uses a FileOpener class to attempt to open the file on the secondary thread. The main thread will open the file and fill the first listbox without any errors. The second listbox will not fill up due to the exceptions occurring on the secondary thread.

Listing 1 contains some relevant code fragments from this program. Pressing the first button will result in an invalid filename at //4, being sent to the FileOpener class causing the secondary thread to generate a checked exception, FileNotFoundException at //1. Pressing the other button will result in a null pointer at //5, being sent to the FileOpener class at making the secondary thread throw an unchecked, NullPointerException at //2.

A key point to note is the primary thread must be able to determine the status of the secondary thread. This can be difficult particularly when the secondary thread may terminate due to an exception. What if you were writing a mission-critical application? How would you report failures in secondary threads to the calling code? After all, the calling code may be able to recover from the problem and try again. At a minimum, the calling code can inform the user that there is an unrecoverable problem and advise them to take some appropriate action. The worst thing that can happen is the calling code will continue as if the secondary thread completed successfully. This will result in errors occurring later, that will be much more difficult to track down.

So, what do we do? You may notice at //3 we are catching the FileNotFoundException generated at //1. Why not catch it and let it pass through our run() method? The answer to this requires some explanation.

Why Not Use The Traditional Try/Catch Technique?

Our first attempt at solving the multithreaded exception handling problem was to devise a solution using traditional exception handling techniques. We simply placed a try/catch block around the start() method. After all, start() instantiates the secondary thread and calls its run() method and the use of try/catch is the natural way of dealing with exceptions. If the code in run() throws any exceptions we should be able to catch them. Let’s see what happens if we try to solve our problem this way. Listing 2 shows some of the code from Listing 1 modified with this dubious idea.

Looking at this code we notice at //1 and //2 we are trying to catch exceptions thrown from the secondary thread by attempting to catch exceptions thrown from the call to start(). Because this code compiles cleanly, your next step may be to get the exception to propagate from the run() method so it can be caught at //2. Listing 3 shows our Runnable class modified with the changes you may make to accomplish this.

Instead of catching the FileNotFoundException in run() as we did in Listing 1, we have removed the try/catch block to let the caller of the run() method handle it. Because the FileNotFoundException is a checked exception, we are required to advertise the fact that our run() method throws this exception by specifying it in the method’s throws clause at //3.

On closer examination, Listings 2 and 3 are ill-fated for two reasons. First, the code in Listing 3 will not even compile because you are not allowed to throw checked exceptions from the run() method. The reason for this is because an override method can only throw exceptions of the same type of the method being overridden or specializations of those types. In other words, because the run() method of the Runnable class does not specify that it throws any checked exceptions, you cannot throw any checked exceptions from your overridden version of run().

The second problem is at //1 and //2 in Listing 2. Even though this code compiles cleanly, it is doomed to fail. Remember that start() instantiates a secondary thread and calls the run() method of the Runnable object asynchronously. Exceptions signal back only to some point on the stack of the affected thread. Because start() creates a new stack, the primary thread will never be notified of any exceptions that occur. What the code in Listing 2 is actually doing is catching exceptions that occur when calling start(), not exceptions that occur in the run() method, which is what you are trying to accomplish.

Making Try/Catch
Around start() "Work"

One way to attempt to solve these problems is by creating a special class that acts like a thread, but also enables us to employ the try/catch model around the start() method for handling exceptions occurring on secondary threads. To accomplish this, we introduce a new class, shown in Listing 4, which will extend java.lang.ThreadGroup. ThreadGroup contains a key method, uncaughtException(), which is called on a thread when an exception occurs that is not handled, that is, caught. When the uncaughtException() method ends, the thread is terminated.

To make our try/catch around start() scheme work, one may attempt to extend the ThreadGroup class at //1, provide a custom version of the start() method at //2, and override the uncaughtException() method at //4. We called this new class a ThreadWrapper. The steps outlined seem necessary so we can intercept the exception occurring on the secondary thread and then have our custom start() method throw it. This will enable the try/catch code from Listing 2 to actually catch the exception that occurred in the secondary thread.

There is one major drawback to the code in Listing 4. This is the use of the join() method at //3. The call to join() is needed in order to support the try/catch technique around the call to start(). The big problem with this is the use of join() effectively makes your code single-threaded again. The join() method will block the main thread until the secondary thread has finished. This completely defeats the purpose of multithreaded programming but was necessary to make the try/catch around start() technique work.

There does not exist a way in Java to use try/catch around your start() method to catch the exceptions thrown from a secondary thread and remain multithreaded. There does, however, exist an elegant way to handle exceptions thrown from secondary threads, that is derived from some of what we have seen so far. A new paradigm for dealing with exceptions is used which builds on the ideas of the JDK 1.1 event model.

Listening For Exceptions

As we have seen, the try/catch model does not extend well into a multithreaded scenario. We need a generic mechanism that allows a main thread to have an arbitrary number of concurrently running secondary threads, each with the ability to communicate exceptions to objects that can deal with these exceptions. This mechanism must allow us to catch and propagate both checked and unchecked exceptions. Unchecked exceptions are relatively straightforward, as the compiler does not force us to either catch these or explicitly declare that we are passing them on to our clients. Checked exceptions are more challenging. Although we are required to handle these programmatically, it still may be desirable to pass this information on for possible recovery (or at least soft shutdown).

The Java 1.1 Event Model introduces the notion of listener classes that can register with GUI components to be notified when events occur on those components. If we abstractly consider exceptions to be events, we can extend this paradigm to address our multithreaded exception handling issues. When an exception occurs in a secondary thread, notification could be sent to one or more other objects that are registered as listeners on that thread. Next, we discuss our approach and introduce the classes used to achieve our goal. For a complete solution, we have three fundamental requirements:

• We need a type of thread that is capable of intercepting ALL of its exceptions, both checked and unchecked. This will allow us to consistently and comprehensively alert listeners of exceptions.

• We need a conduit between the secondary thread and one or more listener objects through which we can pass exception information.

• We need a mechanism that allows one or more listener objects to communicate back to the Runnable object on the thread where the exception occurred. This could be used to attempt recovery, preserve object state, or to perform some cleanup for soft termination.

The SmartThread Class...A Better Thread Than Thread

To address the first fundamental requirement, we introduce the SmartThread class in Listing 5. Like the ThreadWrapper class previously discussed, our SmartThread class extends ThreadGroup at //1. By overriding the ThreadGroup’s uncaughtException() method at //6, the SmartThread is able to intercept all unhandled, unchecked, and checked exceptions. The SmartThread can then notify all registered listener objects of the exception.

In order for SmartThread to notify listener objects of exceptions, it needs to provide an interface for listener objects to register interest. This is done via addThreadExceptionListener() at //4. This method will support multiple listener objects because it is implemented with a Java Vector. When the uncaughtException() method is called, it will iterate over the Vector of listeners at //7 calling their exceptionOccurred() methods. (The astute reader may observe that addThreadExceptionListener() could also be implemented with a Multicaster. We have chosen to use a Vector here for simplicity of illustration. However, the final working program implements a Multicaster.)

Each registered listener’s exceptionOccured() method will be called from //8 with the Runnable object the exception occurred in, the thread the exception occurred on, and the exception thrown that was not handled. It is important to pass the thread because one listener could be listening to multiple threads.

Note that SmartThread is not truly a thread because it extends ThreadGroup by necessity.

It does, however, compose a thread at //2 and //3. In order to avoid the pain of reimplementing the entire thread interface, but still give programmatic access to the composed thread, SmartThread provides a thread getter, getThread(), at //5. For example, the following would allow you to change the priority of the thread composed by the SmartThread to 1.

//...

SmartThread smartThread = new

SmartThread(someRunnable,

"My Smart Java Thread");

smartThread.getThread().setPriority(1);

//...

The ThreadExceptionListener Interface

Exceptions in secondary threads are communicated to listeners through the exceptionOccurred() method, which is defined in the ThreadExceptionListener interface in Listing 6. This addresses our second fundamental requirement.

Classes that wish to be informed of exceptions occurring in other threads should implement this interface. In addition, they should utilize a SmartThread, rather than a regular Java Thread, because the SmartThread class knows how to notify the ThreadExceptionListener of exceptions.

The ThreadExceptionCleanup interface

A ThreadExceptionListener that wishes to communicate back to the Runnable in which the exception occurred, can call its cleanupOnException() method, which is defined in the ThreadExceptionCleanup interface in Listing 7. This addresses our third fundamental requirement. Runnables should implement this interface to participate in cleanup requests originating from a ThreadExceptionListener.

This enables reentry into the Runnable object. When an exception is thrown and not handled, and the uncaughtException() method called, you have left your Runnable object. This class gives you a way to get back into your Runnable object to perform any cleanup or state preservation.

The CheckedThreadException Class

This class allows checked exceptions occurring in a secondary thread to be propagated to listeners in the same manner as unchecked exceptions. Recall that the occurrence of an unhandled unchecked exception results in a direct call to the SmartThread’s implementation of uncaughtException(). From here it is propagated to listeners via our exceptionOccurred() conduit. Because checked exceptions must be explicitly caught (we can’t rethrow them in the run() implementation for reasons discussed earlier), by definition they will never find their way directly to uncaughtException(). We need an indirect way to accomplish this.

CheckedThreadException, shown in Listing 8, extends the unchecked RuntimeException class. CheckedThreadException is a special exception that allows us to throw checked exceptions from a method without requiring a throws clause. When we want to alert our listeners of the occurrence of a checked exception in a secondary thread, we wrapper this exception in a CheckedThreadException and rethrow it. This will result in a call to uncaught

Exception(), allowing us to propagate both checked and unchecked exceptions through the same interface. The CheckedThreadException class also provides access to the original checked exception at //2 and the thread the exception occurred on at //1. This addresses our final fundamental requirement.

Putting It All Together

Using the classes we have introduced, we have modified our initial multithreaded program from Appendix A to effectively deal with the exceptions generated on its secondary threads. The entire modified program is provided in Appendix B with the changes indicated in bold font. Let’s now examine the specific areas we modified utilizing our new classes.

Listing 9 shows the modified FileOpener class. At //1 you will notice that our FileOpener class is implementing our ThreadExceptionCleanup interface. This also requires us to implement the cleanupOnException() method at //5. The cleanupOnException() method will be called if an exception is thrown from our Runnable object and not handled by that object. We are also utilizing the CheckedThreadException class at //4. As discussed, this enables us to throw checked exceptions out of our run() method. We must first catch the checked exception at //3, then wrapper it as an unchecked exception via the CheckedThreadException class. The "decoding" of this exception will occur later in the exceptionOccurred() method.

The cleanupOnExeption() method proves to be very useful at //2. Depending on where the exception was thrown from in the run() method, a BufferedReader object may have been left open and need to be closed. If the exception was due to a file not found, then the BufferedReader would not yet have been opened, as this happens after it is determined that the file was found. However, what if the exception was thrown after //2? If an exception is thrown after //2, the BufferedReader is left open and we have created a resource leak. The cleanupOnException() method at //5 is called by exceptionOccurred() in this case to close the BufferedReader at //6, prior to this thread shutting down.

This particular example could have been accomplished through the proper use of a finally clause, however, in other implementations you may not have a try/catch block to work with because you may not be dealing with checked exceptions, only unchecked exceptions. Therefore, you would not have a try block in which to attach a finally clause.

Listing 10 shows the initial changes made to the PrimaryFrame class. PrimaryFrame now implements the ThreadExceptionListener interface at //1. This enables the PrimaryFrame class to register itself as a listener of uncaught exceptions occurring on its secondary threads via the addThreadExceptionListener() method shown at //3. However, for all of this to work, you must use the SmartThread class instead of the standard Java Thread class. This is accomplished at //2.

The last area to examine is the exceptionOccurred() method. Listing 11 shows the implementation of this method and what we do when we are notified of an exception. This method is called after:

• Creating a SmartThread class.

• Adding a ThreadExceptionListener object.

• Overriding the exceptionOccurred() method in your ThreadExceptionListener object.

• An exception is thrown from your Runnable object.

We see at //1 the exceptionOccurred() method is provided the necessary information to properly deal with the problem being reported. This method knows the Runnable object and the thread the exception occurred on, along with the actual exception thrown. First, we want to know if the exception was really a checked or unchecked exception. At //2 we are checking if the exception is wrappered in a CheckedThreadException, indicating the exception we caught is a checked exception. We then "decode" our CheckedThreadException to see what type of checked exception it is at //3. If we determine the exception was a FileNotFoundException, we attempt to open another file at //4.

Notice here that we don’t register another exception listener. You can register another one, and may want to do so depending on what you would like to happen if this second file is not found. However, be warned that if you register this same class as the listener, you can get the code into an infinite loop fairly easily. It is suggested that in cases like this you register another object as the listener, not the same object. Registering the same object would require some special case code to avoid the infinite loop scenario.

If the exception caught here was not a checked exception, we know it was an unchecked exception. Therefore, we fall to the else clause and call back to our Runnable object via cleanupOnException() at //5. This is done to free the BufferedReader resource allocated in our Runnable object. This was discussed earlier in reference to Listing 9.

Figure 1 represents the flow of control through the final version of our program. Note that when execution returns from the uncaughtException method, the thread will terminate immediately.

Alternate Solution

In the course of our research for this article, we uncovered an alternate solution for multithreaded exception handling in Java published in CurrentProgramming with Java (Lea, D., Addison–Wesley, 1997) This method is called Completion Callbacks. Completion Callbacks involve a predefined interface which an object implements. Secondary threads then must call this interface to indicate success or failure. This allows the object implementing the interface to know whether the secondary thread completed successfully or with an exception.

Completion Callbacks are an excellent approach to the problem addressed in this article but they do require a more tightly coupled relationship between the primary object(s) and the secondary thread(s). In cases where you don’t own the code to be executed in the secondary thread, this could be a problem. The Completion Callback solution also requires the developer to take explicit actions to address both checked and unchecked exceptions. By extending ThreadGroup and overriding the uncaughtException() method the developer is relieved of some of this work, and is also provided with a uniform and generic way of routing exception information to an object able to process it.

Summary

We have shown a solution to enable Java programs to effectively deal with exceptions occurring in a multithreaded environment. We have done so, in part, by using the listener paradigm devised by the JDK 1.1 event model. We have met all of our design goals with our solution and solved all of the problems we have identified relating to completion callbacks. Our solution solves many of the problems of dealing with exceptions in a multithreaded environment. This solution allows you to throw checked and unchecked exceptions from run() methods at will, while ensuring these exceptions will be caught and communicated to predefined listener objects.

This solution is more involved than a callback solution. The callback solution will work fine for certain cases but does not solve all of the problems associated with multithread exception handling. The solution we have provided is complete, robust, and follows the common listener paradigm familiar to Java developers. We have also provided a complete running code example of our final implementation utilizing our classes.

Acknowledgments

The authors wish to thank Gary Craig, Art Jolin, and Bob Love for their efforts reviewing the first draft of this article and for their many useful comments and suggestions. (Appendices A and B are available at Java Report Online—www.javareport.com) n

Joe DeRusso is a Senior Systems Developer with SAS Institute Inc. in Cary, NC. Joe applies existing and emerging Web technologies, such as Java, JavaScript, DHTML, and ActiveX, to prototype and develop Web clients for SAS Software products. He can be contacted at [email protected].

Peter Haggar is an Advisory Software Engineer with IBM in Research Triangle Park, NC. Peter works on emerging Java and Internet technology, focusing on embedded Java and real-time operating systems. He can be contacted at [email protected].