SCOTT'S SOLUTIONS
Programming with threads in Java™ 1.2

By Scott Oaks

Scott Oaks is a Systems Engineer for Sun Microsystems,where he focuses on practical applications of Java technology. He is the co-author (with Henry Wong) of Java Threads. If you'd like to submit a question, send it to soaks@sigs.com or www.sigs.com/java/ask.html.

This month we're going to discuss the changes to the Thread class that have been introduced for Java 1.2. Three methods of the Thread class have been deprecated in 1.2 and there is no obvious replacement for any of them. In fact, avoiding these now deprecated methods will require many of us to re-examine our approaches to programming with threads. Despite the nuisance involved, this is a good thing, because the end result will be a more robust Java program.

The three methods in question are the suspend(), resume(), and stop() methods of the Thread class. The questions that we'll answer about them are why they needed to be deprecated in the first place, and how we need to program our threads in their absence.

All three methods have been deprecated for essentially the same reason -- because they do not work well within Java's model of object synchronization. In very rare circumstances, using these methods can cause the Java virtual machine to deadlock or to become unstable. The circumstances in which they cause a problem are timing dependent and are extremely unlikely to occur -- but if you're running a program when the one-in-a-million event occurs, your Java application (or your entire Web browser) will fail badly.

Synchronized methods (or their equivalent) are an absolute requirement of a threaded system like Java; we can see why if we examine a class that provides a linked list:


public class List {

     class ListNode {
          ListNode next;
          Object data;
     }
     ListNode head = null;
     public synchronized void
                              insert(Object o) {
          ListNode ln = new ListNode();
          if (head == null)
               head = ln;
          else {
               for (p = head;
                              p.next != null)
                    p = p.next;
               p.next = ln;
          }
          ln.data = o;
          ln.next = null;
     }
     public synchronized Object
          delete() {
          ...
     }
}

Here we've done the correct thing by synchronizing the insert() method, because it will be manipulating the internal node references. If you're not familiar with the need to synchronize a method like this, try this exercise: assume that two threads independently call the insert() method on the same object. Now trace through the calls to the insert() method assuming that each thread executes one statement and then yields control to the other thread. If you trace this out, you'll see that the list ends up having only one node inserted; the reference to the other node is lost forever. Synchronization avoids that problem by allowing one thread to complete its insertion before the second thread is allowed to begin its insertion.

In a threaded system, there is no other way to write a method like this -- it must be synchronized. Say that we have a thread that inserts items into the list as part of a typical applet:

public class Test extends Applet
               implements Runnable {
     Thread t;
     public void init() {
          t = new Thread(this);
     }
     public void start() {
          t.resume();
     }
     public void stop() {
          t.suspend();
     }
     public void run() {
          List l = new List();
          while (true) {
               Object o = getSomeData();
               l.insert(o);
          }
     }
}

Our insertion thread is busy obtaining data from a data source and inserting data into the list. Now, without warning, the user surfs to another page and the stop() method is called, suspending the insertion thread. If the insertion thread is in the middle of calling the insert() method, then the insertion thread will be suspended while it is still holding the lock on the list object. Now no other thread will be able to insert or delete an object from the list, because they will be unable to obtain the lock on the list object.

In this example, that doesn't make a difference, because there are no other threads that are interested in accessing the list object. But what if the run() method in the Test class was executing something else when it was suspended? What if, say, it was executing a System.out.println() method? In that case, it might have possession of the lock for the output log; now that the thread is suspended, no other applets will be able to write to the output log. Worse still, what if the thread had possession of a lock related to the internal object store (heap) in the virtual machine? Then no other thread would be allowed to instantiate a new object!

These types of situations are known as deadlock, and as we've just seen, deadlock can occur when the suspend() method is used without regard to its consequences. It's rare, it depends on a very strict timing of sequences between thread execution -- but it is a real possibility. Hence, the suspend() method has been deprecated so that you won't inadvertently stumble into this type of deadlock. Interestingly, there is no problem inherent with the resume() method, but because the resume() method cannot exist without the suspend() method, it too has been deprecated.

The situation with the stop() method is similar, but with different consequences. When a thread that holds an object lock is stopped, it releases the lock that it holds before it terminates. This avoids the problem of deadlock, but it introduces another problem: The stopped thread may have left an internal data structure in an inconsistent and dangerous state. In our linked list example, if the thread is stopped after the node has been added to the list and before the data element is assigned to the node, we'll end up with an uninitialized node in the list.

Clearly, we could have written the insert() method differently so that this example would not pose a problem. In general, however, that's not always possible: There are always certain critical operations that must be carried out atomically, and stopping a thread that is executing one of those operations defeats the atomicity of the operation. And again, many of those operations are integral to the virtual machine itself, so that an applet (or a program) that stops a thread runs the risk of corrupting a data structure deep within the virtual machine, to the detriment of all subsequent applets and threads.

The ability to suspend or stop a thread is still very important, however; now that we know why we can't rely on the suspend() and stop() methods, the question remains: How do we get a particular thread to suspend or to stop?

There are different ways in which to do this, but the simplest way involves setting a flag and letting the thread suspend or stop itself. The important thing to keep in mind is that the suspend and stop methods are not inherently bad things: If you are assured that the thread is not holding any locks, you can safely suspend or stop a thread.

In Java, there is no way for one thread to examine the locks that are held by another thread (unless you write your own locking mechanism, which is a useful solution in some cases). But a thread usually knows what locks it holds, so a thread is able to suspend or stop itself at certain points in time. Consider this thread:

public class TT extends Thread {
     static final int SUSP = 1;
     static final int STOP = 2;
     static final int RUN = 0;
     private int state = RUN;
     public synchronized void
                         setState(int s) {
          state = s;
          if (s == RUN)
               notify();
     }
     private synchronized boolean
                         checkState() {
          while (state == SUSP) {
               try {
                    wait();
               } catch (Exception e) {}
          }
          if (state == STOP) {
               return false;
          }
          return true;
     }
     public void run() {
          while (true) {

               doSomething();

               if (!checkState())

                    break;

          }

     }

}

When this thread calls the checkState() method, it knows that it does not hold any locks, so it is safe for the thread to either suspend itself or stop itself. Now a thread that wants to suspend, resume, or stop a TT thread calls the TT thread's setState() method with the appropriate value, and when the TT thread determines that it is safe to do so, the TT thread will suspend itself (by using the wait() method) or stop itself (by exiting from the run() method).

As an aside, we'll note that the wait() method is subject to the same deadlock problems as the suspend() method: if the thread holds any locks on any objects (other than itself) prior to calling the wait() method, then it will still hold those locks while it is waiting. This has the potential to introduce the same sort of deadlock that we discused earlier.

The difference, of course, is that the programmer can know what object locks the thread holds and ensure that the wait() method is called at the appropriate time. In the above example, we had a convenient place to call the checkState() method (at the bottom of the loop in the run() method); in general, you'll need to ensure that the checkState() method is called often enough so that the thread will not continue to do work after it has been asked to suspend or stop itself.

This solution requires a little more thought on the part of programmers, who now must write threads with the idea that the thread is responsible for changing its state upon request. And it still won't prevent a careless programmer from calling the checkState() method at the wrong time and suspending the thread while it still holds the lock on another object. But it is an effective substitute, and with careful use it provides the equivalent functionality to the deprecated suspend() and stop() methods.



©1997 SIGS Publications, Inc., New York, NY. All Rights Reserved.