CS062, Lecture 22

Threads in Java

Many programmers do not realize that proper programming in an event-driven style requires the use of threads. To see why this is so, let's change the maze-solver program so that we simply call the run method of the maze solver (either iterative or recursive version) rather than starting a separate thread.

When you do this, you notice several things. First the window is not updated until the maze solver has terminated. It neither changes the graphics nor the TextArea in response to changes occurring internally. Second, it no longer responds to the changes in the slider. What is going on?

The problem is that the same thread that is supposed to respond to user events is tied up executing the maze solver. What about the pause method? That causes the event-handling thread to stop executing so that other threads can execute. Unfortunately that doesn't help, as that thread is the only one authorized to handle those events. It is as though you have several tasks to do, with one of them currently taking up all of your time. If you go to sleep for a few hours it doesn't in any way help you to get through those other things you were supposed to be accomplishing. Instead, the most effective thing to do would be to get someone else to handle some of your tasks. That is exactly what is accomplished by creating and starting a new thread.

Let's go back and think about how events are handled in Java. When an event is generated, say by a user action on a GUI component, that event is added to the system's event queue. It is the responsibility of the event-handling thread to take care of all of those events. It is important that there be only one thread removing things from the queue, as if there were several they might both grab the same event at the same time.

If that thread takes too long in responding to an event, the other events will get backed up on the event queue, and some might even be lost (think about those occasions when you have typed ahead when a program is lost in thought -- if you type ahead too much you lose characters). As a result, a very important rule for Java event-handling is that code to handle events must run quickly so that system response time is not degraded.

So what do you do when the response to an event needs to take a long time? You need to spawn a separate thread to handle the response. That is what we did with the maze solver. Let's take a quick look at how we create threads in Java. (In CS 134 we used an objectdraw library class, ActiveObject for the purpose, but that just saved a very small amount of syntactic overhead. As a result we won't use it here.)

There are two ways of creating threads in Java. The first and simplest is to write a class that extends Thread:

   class MazeSolver extends Thread {...}
The constructor of this class should do all of the usual initialization of instance variables, etc. The user must provide a run method:
   public void run() {...}
This is the method that will be executed when the thread starts up. When the run method terminates, the thread itself will terminate. Once it has terminate, it may not be restarted. Instead, you must create a new object from the class.

To start the thread, you send it the message start(). This is a method inherited from the Thread class. It creates a new thread and immediately calls the run method of the object. If you like, you can invoke the start method at the end of the constructor for the object:

    public MyThread(...) {
        ...
        start();
    }
Alternatively, you can send the start message to the object after the constructor has completed:
    MazeSolver ms = new MazeSolver();
    ms.start();
The advantage of this style is that you can do other things between the creation of the object and when you start it running.

Creating threads as extensions of Thread works fine as long as the class being defined does not need to extend another class (remember that Java does not allow a class to extend multiple classes). In that case, there is another style of creating threads that may be used. Begin by declaring the class to implement the Java interface Runnable:

    class RMazeSolver implements Runnable {
        ...
        public void run(){...}
The interface Runnable consists of only the run method. It now takes a bit more work to start execution of the new thread. You must create the object, pass it as a parameter to a Thread constructor, and then send it the start message:
    RMazeSolver ms = new RMazeSolver(...);
    Thread msThread = new Thread(ms);
    msThread.start();
Both of these styles end up with the same result: a separate thread that will continue execution until the run method terminates.

Often one thread will need to communicate with another thread. For example, in the maze program when the slider is moved the event-handling thread (which begins execution of the stateChanged method of the listener object) will need to communicate the changed speed to the thread executing the maze solving code.

This can be accomplished by sending a message to the object whose run method is executing in the separate thread. For example, the MazeSolver class has a method setSpeed, which is called from method stateChanged in the inner class SpeedListener of GraphicsMaze. The method setSpeed changes the instance variable sleepTime of the maze solver object. At the same time that method is being executed by the event-handling thread, the maze solver thread is busy executing the run method of the same object. The next time it executes the command sleep(sleepTime) the value of sleepTime will have been updated, so the maze solver thread will use the new value.

Similarly, one can stop a thread by setting a variable that the run method of the thread can read and use to halt execution. There is an example of this in the maze runner program where the ClearAndChooseListener inner class sends a stopRunning message to the maze solver object. That method sets an instance variable, keepRunning, to be false, and because the main while loop involves keepRunning in its condition, the loop and method terminate. Note that there is no way to force a thread to quit. Instead, there needs to be a method that affects the state of the object so that the thread will notice and terminate its run method.

Monitors and synchronized methods

Programming with threads can be more complicated if both need to access the same data. In particular, problems can arise if two different threads are trying to modify the same variable at the same time.

To get around this, you can set up what is often called a monitor: this is a data structure that only allows access by one thread at a time.

You can declare methods in a class to be synchronized. Synchronized methods are restricted so that only one of the synchronized methods of a class can be active at a time. (Ordinarily, several threads may be executing methods of the object at the same time!)

When a synchronized method is running, a "lock" is established on the synchronized methods, so that none can be executed until the lock is released by the executing thread.

Use pair of methods wait() and notifyAll() when must wait on a condition. Wait releases lock and places thread on a queue waiting to be notified that condition might have changed. NotifyAll wakes all waiting threads and allows them to proceed (or at least determine if they want to proceed). Can also just use notify() to inform one waiting thread (but better to use notifyAll() unless know exactly what is waiting).

Wait, notifyAll, and notify can only be invoked from within synchronized methods (or methods called from within synchronized methods).

See code for a circular buffer. In the program provided there are threads that respectively "produce" and "consume" integers from a buffer of size 5. When a thread tries to consume an integer when none are available, it must wait. Similarly if a thread tries to produce an integer and there is no room in the buffer, the producer must wait. In each case, when an integer has been added or removed from the buffer, the buffer sends a notifyAll() to wake up all of those waiting to use the buffer. Notice that the waits are included in while loops as by the time they have been awakened, the buffer space may be gone.