HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

Persistence is our way to fight the decay of time. We take pictures and film events in order to remember, review and analyze them. We freeze perishable products in order to preserve or transport them over long distances. And in much the same way, computer users save ideas and programs as files on hard disks and transmit them over networks so that they too can be printed and preserved - persisted - over time.

The same technology used to preserve an event (such as a camera taking a picture), can be used to broadcast that event live, over a network, assuring the event's persistence. And with computers, the Serialization API of the Java platform is used to implement the object persistence or to send objects via Remote Method Invocation (RMI).

In a previous article (JDJ Vol. 3, Issue 8), I showed how object serialization might be used to create persistent user interfaces (Reference 1). No threads were created and all the classes used were serializable. However, many Java API classes aren't serializable (i.e., they don't implement java.io.Serializable). One of them is java.lang.Thread.

Why Isn't java.lang.Thread Serializable?
The two main reasons why the Thread class isn't serializable are the dependence of Java threads on the implementation of the Java Virtual Machine (JVM) and the interaction between the concurrent threads. Respecting "The Java Virtual Machine Specification" (Reference 2) and obtaining the best performances when the Java code is run become the main concerns of JVM's vendors.

Some JVMs are better than others because JVM's Specification is flexible, allowing the vendors to adopt different solutions. For example, the stack associated with a thread may or may not be continuous and its size may either be fixed or can change dynamically. In addition, the threads execute a lot of native code (the Java API has many native methods). When this happens, the PC register (program counter) associated with the thread has an undefined value. Hence, the state of a thread isn't always accessible - even to the Java Virtual Machine.

Even if a thread doesn't execute native code, it's still difficult (if not impossible) to detect at runtime all the objects accessed in the thread's code. Note that these objects are stored in the JVM's heap (which is shared between threads.) Some might have their locks locked by another thread. When a thread waits for another thread to release the lock of an object, other threads may acquire the locks of other objects. Using synchronization - without a correct analysis of who might be waiting for who - can lead to deadlock. Usually this analysis can't be made automatically. The communication between concurrent threads through the shared memory is another obstacle for the implementation of the thread persistence at the JVM level.

These are only a few technical reasons why the instances of the java.lang.Thread class can't encapsulate the state of the threads from the JVM's point of view. Therefore there's no point in making java.lang.Thread serializable. This doesn't mean that there aren't other ways to implement thread persistence, however.

What Thread Persistence Actually Means
"The Java Language Specification" (Reference 3) defines a thread as "a single sequential flow of control" within a program. To control the flow, the JVM creates some data structures (the stack, PC, etc.) for each thread. There may also be native resources associated with each Java thread (such as a native thread). The programmer has limited control over a thread through an instance of the java.lang.Thread class.

Each thread has a run() method that's called from the start() method of the Thread object associated with the thread. After the start() method is called, the thread becomes alive. A thread dies when the run() method returns the control to its caller. The System.exit() method kills all live threads.

To be able to define what the persistence of an entity (such as an object or a thread) means, you must know what the state of that entity represents in the moments when this state must be saved. For example, the state of an AWT component consists of the set of the values of its properties (dimension, colors, font types, labels, etc.). Although the AWT components have native peer classes, they can be serialized because the properties that define the state are member variables.

Unlike AWT components, the behavior of a thread isn't standard because it's given by the code of the run() method. It's the developer's task to identify the variables accessed within this code that define the state of the thread at a given moment. Implementing the persistence of a particular thread means:

  1. To define the state of that thread from your point of view, not JVM's;
  2. To be able to save its state when the thread is interrupted;
  3. To be able to restart the thread and restore it to its previous state prior to being interrupted; and
  4. The result of the thread execution must be the same whether the thread was interrupted or not.
What Is Thread Persistence Good For?
Thread persistence is essential for mobile agents, which basically are objects that can move from one host to another within a network. They're very similar to applets in that they run within a container and have one or more sets of init(), start(), stop(), destroy() methods (or equivalents). They also possess a moveTo (URL destination) method which calls the stop() method(s), serializes the Agent object (or its equivalent(s)) and dispatches the bytecode and the state of the agent to the destination host. The agent is then deserialized by the container of the destination host, which calls the start method(s). It's the agent developer's responsibility to override the start() and stop() methods that must preserve and retrieve the execution state, respectively. This means that the programmer must implement the persistence of the agent's thread(s). For more information about agents, see "Design of Multi-Agent Programming Libraries for Java" (Reference 4).

Mobility is the main feature of mobile agents. Other interesting, optional facilities are communication through messages, the means to clone and join agents, and even artificial intelligence. This is why the agents need a special container. If you only need to move tasks to dynamically balance a distributed system, then you may implement mobility in ordinary applications that use RMI or sockets to transmit the current state of mobile tasks (or threads).

The applications, which run for a long time to accomplish a certain task, may also benefit from thread persistence. These applications can become more friendly if they allow the user to suspend them and to resume the task after an undefined period of time. The state of the threads can also be saved automatically from time to time so the user doesn't have to start all over again after a crash.

In all three above scenarios, the programmer's main task is to implement the persistence of the threads. I will give you an example of how to do that.

The PersThread Application
To be able to focus on thread persistence I chose a simple example. The Pers-Thread application opens a window that contains a Close button and a canvas object. The application uses a counter, initialized with 0, which is incremented every 100 milliseconds until it reaches MAX_COUNTER == 300. The counter's value is used to compose a color like this:

Color c;
if (counter <100)
c = new Color(counter/100f, 0, 0);
else if (counter <200)
c = new Color(1, (counter-100)/100f, 0);
else
c = new Color(1, 1, (counter-200)/100f);

The color and its RGB components are shown in the application's window (see Figure 1).

Figure 1
Figure 1:

The PersThread class extends java.awt.Frame and implements java.lang.Runnable (see Listing 1). The counter is run in increments within a thread whose run() method is member of a PersThread object. This object encapsulates the state of the thread and represents the window of the application. The state of the application's user interface, the counter's value and the value of the threadStage member variable compose the thread's state. The static member variables of the PersThread instance aren't parts of the state of the thread (they won't be serialized).

The user interface is built in the Pers-Thread() constructor, which sets the background color, the title and the BorderLayout manager of the window. Next it creates and adds a PTCanvas component and a Close button. A PTAdapter object is created and registered as an event listener to the button and the frame. PTCanvas and PTAdapter are inner classes. The counter variable is given in increments in the computing() method, called from run() (see Listing 1). The limit parameter indicates how many increments the counter must have. After each increment, the canvas is repainted and a 100-millisecond pause is taken.

The run() method is executed in three stages (i.e., it calls computing() three times). The number of the current stage is stored in the threadStage variable. If the counter is in the red zone (i.e. 0 <= counter <="100)," then threadStage="=" 0. If the counter is in the green zone (i.e. 101 <="counter" <="200)," then threadStage="=" 1. If the counter is in the blue zone (i.e. 201 <="counter" <="300)," then threadStage="=" 2.

This section has identified the state of this thread. The next two sections show how to interrupt this thread and serialize its state.

Thread Breakpoints
The run() method calls computing() three times. To simulate a real-world application, I chose not to interrupt the computing() method, whose code may be seen as a sequence that once started, must also be completed. If the Thread.sleep() call throws an InterruptedException, then the mustBeInterrupted flag is set to true. Before returning the control, the computing() method checks this flag. If its value is true, then the interrupt() method of the current thread is called.

The run() method calls breakPoint() between two computing() calls (see Listing 1). The breakPoint() method uses Thread.interrupted() to check if the interrupt() method of the current thread was called. If interrupted() returns true, then breakPoint() calls the serialize() method of the PersThread object to save the state of the thread. Then it calls System.exit() to close the application.

Note that the interrupt() method doesn't stop the thread. It just signals to the thread that it should arrange its own death. You must not use the stop() method (which was deprecated in JDK 1.2) instead of interrupt(), because stop() kills the thread without warning. After a stop() call, the state of a thread might remain inconsistent.

Implementing Thread Persistence
Now you know what thread persistence means and what it's good for. The next question is how to implement the persistence of the thread.

The state of that thread consists of the state of the application's user interface, the value of the counter and the value of the threadStage member variable. All this data, which defines the thread's state, is either contained or referenced directly or indirectly by the member variables of a Pers-Thread object. The serialize() method of this object calls writeObject() from the ObjectOutputStream class to save into a file the state of the thread whose run() method counter is in increments.

ObjectOutputStream s = new ...
s.writeObject(this);
s.flush();
s.close();

If the Close button is clicked and the thread is alive then the interrupt() method is called (see the appExit() method of the PTAdapter class from Listing 1). Then the Thread.sleep() call from computing() will throw an InterruptedException. The catch of this exception is that it will neutralize the effect of the interrupt() call. However, the mustBeInterrupted flag will be set to true. Before returning the control, computing() will call the interrupt() method again. Therefore, when the breakPoint() method gains the control, Thread.interrupted() will return true. Then breakPoint() will call serialize() and System.exit(). Note that breakPoint() can gain the control only between two calls of computing().

The next time the application is run the main() method will have to restore the state of the thread and restart it. The readObject() method of the ObjectInputStream class will be called to deserialize the state of the thread. If this operation succeeds, then the window of the application will be shown on the screen at the same position with the help of the show() method. The components will have the same state. Then the main() method will create a new thread call its start() method.

ObjectInputStream s = ...
pt = (PersThread) s.readObject();
s.close();
pt.show();
thread = new Thread(pt);
thread.start();

The start() method calls the run() method of the PersThread instance. To continue the execution of the application from the point where it was interrupted, the run() method uses the value of the threadStage variable. The computing() calls - which were completed at the previous run of the application - are now skipped. For example, if threadStage is 1, then the application was interrupted after the first computing() call (before the counter passes from the red zone to the green zone). The first computing() call is then skipped so the counter doesn't have to go through the red zone again. If the user clicks the Close button when the counter is in the green zone, then the application is interrupted right before the counter passes into the blue zone. If the counter manages to enter the blue zone before the user presses the Close button, then the application can't be interrupted anymore because from this point, no more breakPoint() calls are made.

If the deserialization fails (e.g., FileNotFoundException is thrown), then the main() method creates a new instance of the PersThread class, shows the window and starts the thread.

pt = new PersThread();
pt.show();
thread = new Thread(pt);
thread.start();

An important observation is that the result of the application's execution is the same, no matter whether the application is interrupted or not (i.e., the white color is shown after 300 increments of the counter). In addition, the execution time is almost the same because the main actions are the same (i.e., the computing() method is completed three times whether or not the application is interrupted).

Synchronization and Inner Classes
The code of the PersThread application is executed within three different threads. The first thread is created by the Java Virtual Machine for the main() method of the application. (The run() method of this thread calls main().) The second thread is created in the main() method. The run() method of the second thread calls computing() for increments from the counter. I implemented the persistence only for this thread. The third thread is an EventDispatchThread of AWT. It takes events off the EventQueue of AWT and dispatches them to the appropriate AWT components. Most code of the PTAdapter and PTCanvas classes is executed within the third thread.

After the computing() method calls repaint(), a PaintEvent is dispatched to the PTCanvas component. The event is processed and the paint() method is executed within the AWT thread. The paint() method needs the counter's value to calculate the color shown in the canvas of the application's window. The counter member variable is accessed in two separate concurrently running threads (the second and the third). Therefore, the code that increments the counter (in computing()) and the getCounter() method have been synchronized.

Note that in this particular application the synchronized keyword isn't actually necessary because the counter variable is atomic (its type is int), and there's no negative effect if getCounter() is called between the two accesses of the counter member variable from "if (counter <max_counter)" and "counter++;". This is a very rare combination of coincidences. For example, if the type of the counter variable had been nonatomic (e.g., long), then the synchronization of the accesses to this variable would have been compulsory.

The use of synchronization without reason is a bad idea because it reduces the performance of the application. In this article, my purpose was to show that the persistence could be implemented even for concurrent threads. You usually have to synchronize access to the member variables that are used and modified in concurrent threads. The local variables and the parameters of a method can't be accessed from concurrent threads because their values are stored in the stack associated with the thread that calls the method.

A second important observation is that the value of the counter is used in more than one place in the code of the paint() method of the PTCanvas component. This method composes the color shown in the canvas of the application's window. The counter's value is copied into a local variable because the computing() method can increment the counter anytime. Though the paint() method can access the counter directly (because PTCanvas is inner class), the right procedure is to call getCounter(). There's no way to protect the member variables against the methods of the inner classes. You must be very careful when the methods of a class and the methods of the inner classes are executed within concurrent threads.

Application's Exit
The getCounter() method is also called from the appExit() method of the PTAdapter class. When the user tries to close the application's window or clicks the Close button, an event is generated and passed to one of the actionPerformed() or windowClosing() methods. Each of these two methods calls the appExit() method of the PTAdapter instance that was registered as listener to the application's window and to the Close button. The appExit() method calls the interrupt() method of the thread created in the main() method if this thread is alive and if the value returned from getCounter() is less than MAX_COUNTER. Otherwise, the .ser file created in the serialize() method (when the application was interrupted last) is deleted, before the System.exit() call. This operation allows the application to be restarted with counter == 0 after its execution was completed without interruption.

The System.exit() call from breakPoint() is the simplest way to close the PersThread application, but it isn't the most refined because it kills all threads without notice. If an application must continue its execution after the state of a thread was serialized, then the breakPoint() method must be modified. For example, it could throw an InterruptedException instead of System.exit() call. The run() method of the thread would catch this exception and would use the return instruction in the catch block. From this point, the thread would be dead and the application would continue its execution.

Thread Persistence For Real-World Applications
The PersThread application is simple because it implements the persistence of a single thread. A complex application from the real world might be needed to implement the persistence for more than just a thread. If the threads are independent, then those with low priority might be interrupted and their state might be saved on disk to release memory resources for the high-priority threads. Note that the state of a thread can sometimes consist of huge data structures.

The implementation of the persistence may be very complex for concurrent threads. You must analyze all the situations where a thread might wait for another thread to perform an unlock operation. However, the programmer's task is simplified by the fact that the lock and unlock operations are automatically performed.

There are situations when the persistence of a thread simply can't be implemented. A typical situation is when you may not block a shared resource (e.g., a database, a server, etc.), or, if you block it, then you must unblock it as soon as possible - not after an indefinite period of time.

Summary
The programming technique presented in this article is typically used in distributed systems to move tasks from one host to another. Computing-intensive applications can become friendlier with the help of thread persistence, which allows the user to interrupt the applications and resume them after an undefined time or a crash.

References

  1. Andrei Cioroianu, "Persistent User Interface for Multiuser Applications", Java Developer's Journal, Vol. 3, Issue 8, www.javadevelopersjournal.com/
  2. Tim Lindholm and Frank Yellin, The Java Virtual Machine Specification, Addison Wesley, http://java.sun.com/docs/books/vmspec/
  3. James Gosling, Bill Joy, Guy Steele, The Java Language Specification, Addison Wesley, http://java.sun.com/docs/books/jls/
  4. Takashi Nishigaya, Design of Multi-Agent Programming Libraries for Java, Fujitsu Laboratories Ltd., www.fujit-su.co.jp/hypertext/free/kafka/paper/

About the Author
Andrei is an independent Java developer and writer. You can visit his "(a) Java Developer's Page" at www.geocities.com/SiliconValley/Horizon/6481/, and you're invited to send questions or comments about this article to [email protected]

	

Listing 1: The PersThread Application

// PersThread.java
... 

public class PersThread extends Frame 
                        implements Runnable {
    final static String SER_FILE_NAME = ...
    final static int MAX_COUNTER = 300;
    static Thread thread = null;
    private int threadStage = 0;
    private int counter = 0;
    private PTCanvas canvas;

    private PersThread() {
        // Builds the user interface
        ...
    }

    synchronized int getCounter() {
        return counter;
    }

    void computing(int limit) {
        boolean mustBeInterrupted = false;
        while (limit > 0) {
            synchronized (this) {
                if (counter < MAX_COUNTER)
                    counter++;
            }
            canvas.repaint();
            limit--;
            try {
                Thread.sleep(100);
            }
            catch (InterruptedException e) {
                mustBeInterrupted = true;
            }
        }
        if (mustBeInterrupted)
            Thread.currentThread().interrupt();
    }

    void breakPoint() {
        if (Thread.interrupted()) {
            serialize();
            System.exit(0);
        }
    }

    // The run() method of the persistent thread
    public void run() {
        if (threadStage == 0) {
            computing(100);
            threadStage = 1;
        }
        breakPoint();
        if (threadStage == 1) {
            computing(100);
            threadStage = 2;
        }
        breakPoint();
        if (threadStage == 2) {
            computing(100);
            threadStage = 3;
        }
    }

    public void serialize() {
        // Tries to serialize the thread's state
        ...
    }

    public static void main(String args[]) {
        // Tries to deserialize the thread's state
        // If deserialization fails then
        // a new PersThread instance will be created
        ...
    }

// Inner class
class PTCanvas extends Canvas {
    ...

    public void paint(Graphics g) {
        // Makes a copy of the counter member var.
        int counter = getCounter();
        // Paints the canvas
        ...
    }
}

// Inner class
class PTAdapter extends WindowAdapter 
    implements ActionListener, Serializable {

    public void actionPerformed(ActionEvent e) {
        appExit();
    }

    public void windowClosing(WindowEvent e) {
        appExit();
    }

    void appExit() {
        if (thread != null && thread.isAlive() 
            && getCounter() < MAX_COUNTER)
            thread.interrupt();
        else {
            // Deletes the .ser file
            ...
            System.exit(0);
        }
    }
}

}

  
      
 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.