Chapter 8. Threads

CONTENTS
  •  8.1 Introducing Threads
  •  8.2 Threading an Applet
  •  8.3 Synchronization
  •  8.4 Scheduling and Priority
  •  8.5 Thread Groups
  •  8.6 Thread Performance

At the heart of designing computer systems and software lies the problem of managing time, specifically, scheduling what to do and when to do it. We take for granted that modern computer systems such as desktop computers can manage many applications running concurrently and produce the effect that the software is running simultaneously. Of course we know that, for the most part, our single processor computers can do only one thing at a time. The magic is performed by slight of hand in the operating system, which juggles applications and turns its attention from one to the next so quickly that they appear to run at once.

In the old days, the unit of concurrency for such systems was the application or process. To the OS, a process was more or less a black box that decided what do to on its own. If an application required greater concurrency, it could get it only by running multiple processes and communicating between them, but this was a heavyweight approach and not very elegant. Later, the concept of threads was introduced. Threads provide fine-grained concurrency within a process, under the application's own control. Threads have existed for a long time but have historically been tricky to use. In Java, support for threading is built right into the language, which means it's easier to work with threads. It also means that Java's APIs take full advantage of threading. So it's important that you become familiar with threads early in your exploration of Java.

Threads are integral to the design of many Java APIs. For example, when we look at GUI programming later in this book, you'll see that a component's paint() method isn't called directly by the application but rather by a separate master thread within the Java runtime system. At any given time, there may be many such background threads, performing activities in parallel with your application. In fact, it's easy to get half a dozen or more threads running in an application without even trying, simply by loading images, updating the screen, playing audio, and so on. But these things happen behind the scenes; you don't normally have to worry about them. In this chapter, we'll talk about writing applications that create and use their own threads explicitly.

8.1 Introducing Threads

Conceptually, a thread is a flow of control within a program. A thread is similar to the more familiar notion of a process, except that multiple threads within the same application share much of the same state in particular, they run in the same address space. It's not unlike a golf course, which many golfers use at the same time. Sharing the same address space means that threads share a working area. They have access to the same objects including static and instance variables within their application. However, threads have their own copies of local variables, just as players share the golf course, but not personal things like clubs and balls.

Multiple threads in an application have the same problems as the golfers in a word, synchronization. Just as you can't have two sets of players blindly playing the same green at the same time, you can't have several threads trying to access the same variables without some kind of coordination. Someone is bound to get hurt. A thread can reserve the right to use an object until it's finished with its task, just as a golf party gets exclusive rights to the green until it's done. And a thread that is more important can raise its priority, asserting its right to play through.

The devil is in the details, of course, and those details have historically made threads difficult to use. Fortunately Java makes creating, controlling, and coordinating threads simpler by integrating some of these concepts directly into the language.

It is common to stumble over threads when you first work with them because creating a thread exercises many of your new Java skills all at once. You can avoid confusion by remembering there are always two players involved in running a thread: a Java language object that represents the thread itself and an arbitrary target object that contains the method the thread is to execute. Later, you will see that it is possible to play some sleight of hand and combine these two roles, but that special case just changes the packaging, not the relationship.

8.1.1 The Thread Class and the Runnable Interface

A new thread is born when we create an instance of the java.lang.Thread class. The Thread object represents a real thread in the Java interpreter and serves as a handle for controlling and coordinating with its execution. With it, we can start the thread, wait for it to complete, cause it to sleep for a time, or interrupt its activity. The constructor for the Thread class accepts information about where the thread should begin its execution. Conceptually, we would like to simply tell it what method to run, but since there are no pointers to methods in Java (not in this sense anyway), we can't specify one directly. Instead, we have to take a short detour and use the java.lang.Runnable interface to create an object that contains a "runnable" method. Runnable defines a single, general-purpose method.

public interface Runnable {      abstract public void run( ); }

Every thread begins its life by executing the run() method in a Runnable object, the "target object" that was passed to the thread's constructor. The run() method can contain any code, but it must be public, take no arguments, have no return value, and throw no checked exceptions.

Any class that contains an appropriate run() method can declare that it implements the Runnable interface. An instance of this class is then a runnable object that can serve as the target of a new thread. If you don't want to put the run() method directly in your object (and very often you don't), you can always make an adapter class that serves as the Runnable for you. The adapter's run() method can then call any method it wants after the thread is started. We'll show examples of these options later.

8.1.1.1 Creating and starting threads

A newly born thread remains idle until we give it a figurative slap on the bottom by calling its start() method. The thread then wakes up and proceeds to execute the run() method of its target object. start() can be called only once in the lifetime of a thread. Once a thread starts, it continues running until the target object's run() method returns (or throws an unchecked exception of some kind). The start() method has a sort of evil twin method called stop(), which kills the thread permanently. However, this method is deprecated and should no longer be used. We'll explain why and give some examples of a better way to stop your threads later in this chapter. We will also look at some other methods you can use to control a thread's progress while it is running.

Now let's look at an example. The following class, Animation, implements a run() method to drive its drawing loop:

class Animation implements Runnable {     public void run( ) {         while ( true ) {             // draw Frames             ...         }     } }

To use it, we create a Thread object, passing it an instance of Animation as its target object, and invoke its start() method. We can perform these steps explicitly:

Animation happy = new Animation("Mr. Happy"); Thread myThread = new Thread( happy ); myThread.start( );

Here we have created an instance of our Animation class and passed it as the argument to the constructor for myThread. When we call the start() method, myThread begins to execute Animation's run() method. Let the show begin!

This situation is not terribly object-oriented. More often, we want an object to handle its own threads, as shown in Figure 8-1, which depicts a Runnable object that creates and starts its own thread. We'll show our Animation class performing these actions in its constructor, although in practice it might be better to place them in a more explicit controller method (e.g., startAnimation()).

Figure 8-1. Interaction between Animation and its thread

figs/lj2.0801.gif

class Animation implements Runnable {      Thread myThread;      Animation (String name) {          myThread = new Thread( this );          myThread.start( );      }      ... }

In this case, the argument we pass to the Thread constructor is this, the current object (which is a Runnable). We keep the Thread reference in the instance variable myThread in case we want to interrupt the show or exercise some other kind of control later.

8.1.1.2 A natural-born thread

The Runnable interface lets us make an arbitrary object the target of a thread, as we did in the previous example. This is the most important general usage of the Thread class. In most situations in which you need to use threads, you'll create a class (possibly a simple adapter class) that implements the Runnable interface.

However we'd be remiss not to show you the other technique for creating a thread. Another design option is to make our target class a subclass of a type that is already runnable. As it turns out, the Thread class itself conveniently implements the Runnable interface; it has its own run() method, which we can override directly to do our bidding:

class Animation extends Thread {      public void run( ) {          while ( true ) {              // draw Frames              ...          }      } }

The skeleton of our Animation class looks much the same as before, except that our class is now a subclass of Thread. To go along with this scheme, the default constructor of the Thread class makes itself the default target. That is, by default, the Thread executes its own run() method when we call the start() method, as shown in Figure 8-2. So now our subclass can just override the run() method in the Thread class. (Thread itself defines an empty run() method.)

Figure 8-2. Animation as a subclass of Thread

figs/lj2.0802.gif

Now we create an instance of Animation and call its start() method (which it also inherited from Thread):

Animation bouncy = new Animation("Bouncy"); bouncy.start( );

Alternatively, we can have the Animation object start its thread when it is created, as before:

class Animation extends Thread {         Animation (String name) {          start( );      }      ... }

Here our Animation object just calls its own start() method when an instance is created. (Again, it's probably better form to start and stop our objects explicitly after they're created rather than starting threads as a hidden side effect of object creation. But this serves the example well.)

Subclassing Thread may seem like a convenient way to bundle a thread and its target run() method. However, this approach often isn't the best design. If you subclass Thread to implement a thread, you are saying you need a new type of object that is a kind of Thread, which exposes all of the public API of the Thread class. While there is something very satisfying about taking an object that's primarily concerned with performing a task and making it a Thread, the actual situations where you'll want to create a subclass of Thread should not be very common. In most cases, it is more natural to let the requirements of your program dictate the class structure. If you find you're subclassing Thread a lot, you may want to examine whether you are falling into the design trap of making objects that are really glorified functions.

8.1.1.3 Using an adapter

Finally, as we have suggested, we can build an adapter class to give us more control over how to structure the code. It is particularly convenient to create an anonymous inner class that implements Runnable and invokes an arbitrary method in our object. This almost gives the feel of starting a thread and specifying an arbitrary method to run, as if we had method pointers. For example, suppose that our Animation class provides a method called startAnimating(), which performs setup (loads the images, etc.) and then starts a thread to perform the animation. We'll say that the actual guts of the animation loop are in a private method called drawFrames(). We could use an adapter to run drawFrames() for us:

class Animation {        public void startAnimating( ) {         // do setup, load images, etc.         ...         // start a drawing thread         Thread myThread = new Thread ( new Runnable( ) {            public void run(  ) { drawFrames( ); }         } );         myThread.start( );     }        private void drawFrames( ) {         // do animation ...     } }

In this code, the anonymous inner class implementing Runnable is generated for us by the compiler. We create a thread with this anonymous object as its target and have its run() method call our drawFrames() method. We have avoided implementing a generic run() method in our application code at the expense of generating an extra class.

Note that we could be even more terse in the previous example by simply having our anonymous inner class extend Thread rather than implement Runnable. We could also start the thread without saving a reference to it if we won't be using it later.

new Thread( ) {    public void run(  ) { drawFrames( ); } }.start( );

8.1.2 Controlling Threads

We have seen the start() method used to bring a newly created thread to life. Several other instance methods let us explicitly control a thread's execution:

  • The sleep() method causes the current thread to wait for a designated period of time, without consuming much (or possibly any) CPU time.

  • The methods wait() and join() coordinate the execution of two or more threads. We'll discuss them in detail when we talk about thread synchronization later in this chapter.

  • The interrupt() method wakes up a thread that is sleeping in a sleep() or wait() operation or is otherwise blocked on a long I/O operation.[1]

8.1.2.1 Deprecated methods

We should also mention that there are three deprecated thread control methods: stop(), suspend(), and resume(). The stop() method complements start(); it destroys the thread. start(), and the deprecated stop() method, can be called only once in the life cycle of a thread. By contrast, the deprecated suspend() and resume() methods were used to arbitrarily pause and then restart the execution of a thread.

Although these deprecated methods still exist in the latest version of Java (and will probably be there forever), they shouldn't be used in new code development. The problem with both stop() and suspend() is that they seize control of a thread's execution in an uncoordinated and harsh way. This makes programming difficult; it's not always easy for an application to anticipate and properly recover from being interrupted at an arbitrary point in its execution. Moreover, when a thread is seized using one of these methods, the Java runtime system must release all its internal locks used for thread synchronization. This can cause unexpected behavior and, in the case of suspend(), can easily lead to deadlock.

A better way to affect the execution of a thread which requires just a bit more work on your part is by creating some simple logic in your thread's code to use monitor variables (flags), possibly in conjunction with the interrupt() method, which allows you to wake up a sleeping thread. In other words, you should cause your thread to stop or resume what it is doing by asking it nicely rather than by pulling the rug out from under it unexpectedly. The thread examples in this book use this technique in one way or another.

8.1.2.2 The sleep( ) method

We often need to tell a thread to sit idle, or "sleep," for a fixed period of time. While a thread is asleep, or otherwise blocked on input of some kind, it doesn't consume CPU time or compete with other threads for processing. For this, we can either call the thread's sleep() instance method or use the static convenience method Thread.sleep(), which affects the currently executing thread. In either case, the call causes the thread to go idle for a specified number of milliseconds:

try {     // The current thread     Thread.sleep( 1000 );     // particular thread     someThread.sleep( 500 ); } catch ( InterruptedException e ) {     // someone woke us up prematurely }

The sleep() method may throw an InterruptedException if it is interrupted by another thread via the interrupt() method. As you see in the previous code, the thread can catch this exception and take the opportunity to perform some action such as checking a variable to determine whether or not it should exit or perhaps just perform some housekeeping and then go back to sleep.

8.1.2.3 The join( ) method

Finally, if you need to coordinate your activities with another thread by waiting for the other thread to complete its task, you can use the join() method. Calling a thread's join() method causes the caller to block until the target thread completes. Alternatively, you can poll the thread by calling join() with a number of milliseconds to wait. This is a very coarse form of thread synchronization. Later in this chapter, we'll look at a much more general and powerful mechanism for coordinating the activities of threads: wait() and notify().

8.1.2.4 The interrupt( ) method

Earlier we described the interrupt() method as a way to wake up a thread that is idle in a sleep(), wait(), or lengthy I/O operation. This is indeed the prescribed functionality of the method. However, historically, this has been a weak spot, and Java implementations have had trouble getting it to work correctly in all cases. In early Java VMs (prior to Version 1.1), interrupt did not work at all. In more recent versions there are still problems with interrupting I/O calls. By an I/O call we mean when an application is blocked in a read() or write() method, moving bytes to or from a source such as a file or the network. In this case Java is supposed to throw an InterruptedIOException when the interrupt() is performed. However this has never been reliable across all Java implementations. To address this in Java 1.4, a new I/O framework (java.nio) was introduced with one of its goals being to specifically address these problems. When the thread associated with an NIO operation is interrupted, the thread wakes up and the I/O stream (called a "channel") is automatically closed. (See Chapter 11 for more about the NIO package.)

8.1.3 Death of a Thread

A thread continues to execute until one of the following things happens:

  • It explicitly returns from its target run() method.

  • It encounters an uncaught runtime exception.

  • The evil and nasty deprecated stop() method is called.

So what happens if none of these things occurs, and the run() method for a thread never terminates? The answer is that the thread can live on, even after what is ostensibly the part of the application that created it has finished. This means we have to be aware of how our threads eventually terminate, or an application can end up leaving orphaned threads that unnecessarily consume resources.

In many cases, we really want to create background threads that do simple, periodic tasks in an application. The setDaemon() method can be used to mark a thread as a daemon thread that should be killed and discarded when no other application threads remain. Normally, the Java interpreter continues to run until all threads have completed. But when daemon threads are the only threads still alive, the interpreter will exit.

Here's a devilish example using daemon threads:

class Devil extends Thread {     Devil( ) {         setDaemon( true );         start( );     }     public void run( ) {         // perform evil tasks     } }

In this example, the Devil thread sets its daemon status when it is created. If any Devil threads remain when our application is otherwise complete, the runtime system kills them for us. We don't have to worry about cleaning them up.

Daemon threads are primarily useful in standalone Java applications and in the implementation of the Java runtime system itself, but not component applications such as applets. Since an applet runs inside another Java application, any daemon threads it creates can continue to live until the controlling application exits probably not the desired effect. A browser or any other application can use ThreadGroups to contain all the threads created by subsystems of an application and then clean them up if necessary.

One final note about killing threads gracefully. A very common problem new developers encounter the first time they create an application using an AWT or Swing component is that their application never exits; the Java VM just seems to hang indefinitely after everything is finished. This is because when working with graphics, Java has created an AWT thread to process input and painting events. The AWT thread is not a daemon thread so it doesn't exit automatically when other application threads have completed, and the developer must call System.exit() explicitly. (If you think about it, this makes sense. Since most GUI applications are event-driven and simply wait for user input, they would otherwise simply exit after their startup code completed.)

8.2 Threading an Applet

Applets are embeddable Java applications that are expected to start and stop themselves on command. Applets may be asked to start and stop themselves any number of times. A Java-enabled web browser normally starts an applet when the applet is displayed and stops it when the user moves to another page or (in theory) when the user scrolls the applet out of view. To conform to the semantics of the API, we would like an applet to cease its nonessential activity when it is stopped and resume it when started again. (If you're not familiar with applets, you may want to take a look at Chapter 22 at this point.)

In this section, we will build UpdateApplet, a simple base class for an applet that maintains a thread to automatically update its display at regular intervals. Although we're building an applet here, the general techniques are important for all threaded applications.

UpdateApplet handles the basic starting and stopping behavior for us:

//file: UpdateApplet.java  public class UpdateApplet extends java.applet.Applet     implements Runnable {     private Thread updateThread;     int updateInterval = 1000;        public void run( ) {         while ( updateThread != null ) {              try {                 Thread.sleep( updateInterval );              } catch (InterruptedException e ) {                  return;               }              repaint( );         }     }        public void start( ) {         if ( updateThread == null ) {             updateThread = new Thread(this);             updateThread.start( );         }     }     public void stop( ) {         if ( updateThread != null ) {             Thread runner = updateThread;             updateThread = null;  // flag to quit             runner.interrupt( );   // wake up if asleep         }     }  }

UpdateApplet is a Runnable object that alternately sleeps and calls its repaint() method. (There's nothing to paint, though, so running this applet is kind of boring. Later in this section, we'll subclass it to implement a digital clock.) It has two other public methods: start() and stop(). These are methods of the Applet class we are overriding; don't confuse them with the similarly named methods of the Thread class. These start() and stop() methods are called by the web browser or applet viewer to tell the applet when it should and should not be running.

UpdateApplet illustrates an environmentally friendly way to deal with threads in a simple applet. UpdateApplet simply dismisses its thread each time the applet is stopped and recreates it if the applet is restarted. When UpdateApplet's start() method is called, we first check to make sure there is no currently executing updateThread. We then create one to begin our execution. When our applet is subsequently asked to stop, we set a flag indicating that it should stop and then make sure it is awake by invoking its interrupt() method. In our stop() method, we set updateThread to null, which serves three purposes: it allows the garbage collector to clean up the dead Thread object; it indicates to UpdateApplet's start() method that the thread is gone so that another one can be started when necessary; and it serves as the flag to indicate to the running thread that it is time to quit. If you feel that we have overburdened this variable, you might consider using a separate boolean variable for the flag condition.

One thing about Applets: in truth, an Applet's start() and stop() methods are guaranteed to be called in sequence. As a result, we shouldn't have to check for the existence of updateThread in start(). (It should always be null.) However, it's good programming practice to perform the test. If we didn't, and for some reason stop() were to fail at its job, we might inadvertently start a lot of threads.

With UpdateApplet doing all the work for us, we can now create the world's simplest clock applet with just a few lines of code. Figure 8-3 shows our Clock. (This might be a good one to run on your Java wristwatch.)

Figure 8-3. The Clock applet

figs/lj2.0803.gif

Here's the code:

//file: Clock.java public class Clock extends UpdateApplet {     public void paint( java.awt.Graphics g ) {         g.drawString( new java.util.Date(  ).toString( ), 10, 25 );    } }

The java.util.Date().toString() method creates a string that contains the current time.

Our Clock applet provides a good example of a simple thread; we don't mind throwing it away and subsequently rebuilding it if the user should happen to wander on and off our web page a few times. But what if the task that our thread handles isn't so simple? What if, for instance, we have to open a socket and establish a connection with another system? This isn't strictly a function of the thread of course. But a more general solution might be to have the thread set a timer for itself and clean up at some point in the future.

Now if you're concerned about being so cavalier in creating and discarding Thread objects, you might also ask if we couldn't simply do a little more logic and save our thread. Perhaps we could teach the applet's start() method to have the existing thread start up again rather than having to create a new thread. It should be apparent how to go about this using the wait() and notify() methods after you read the next section on thread synchronization.

However, an issue with applets is that we have no control over how a user navigates web pages. For example, say a user scrolls our applet out of view, and we pause our thread. Now we have no way of ensuring that the user will bring the applet back into view before moving to another page. And actually, the same situation would occur if the user simply moves on to another page and never comes back. That's not a problem in this simple example, but there may be cases in which we need to do some application cleanup before we die. For this situation the Applet API gives us the destroy() method. destroy() is called by the Java runtime system when the applet is going to be removed (often from a cache). It provides a place at which we can free up any resources the applet is holding.

8.3 Synchronization

Every thread has a life of its own. Normally, a thread goes about its business without any regard for what other threads in the application are doing. Threads may be time-sliced, which means they can run in arbitrary spurts and bursts as directed by the operating system. On a multiprocessor system, it is even possible for many different threads to be running simultaneously on different CPUs. This section is about coordinating the activities of two or more threads so that they can work together and not collide in their use of the same address space (coordinating their play on the golf course).

Java provides a few simple structures for synchronizing the activities of threads. They are all based on the concept of monitors, a widely used synchronization scheme (developed by C.A.R. Hoare). You don't have to know the details about how monitors work to be able to use them, but it may help you to have a picture in mind.

A monitor is essentially a lock. The lock is attached to a resource that many threads may need to access, but that should be accessed by only one thread at a time. It's very much like a restroom with a door that locks. If the resource is not being used, the thread can acquire the lock and access the resource. By the same token, if the restroom is unlocked, you can enter and lock the door. When the thread is done, it relinquishes the lock, just as you unlock the door and leave it open for the next person. However, if another thread already has the lock for the resource, all other threads have to wait until the current thread finishes and releases the lock. This is just like when the restroom is locked when you arrive: you have to wait until the current occupant is done and unlocks the door.

Fortunately, Java makes the process of synchronizing access to resources quite easy. The language handles setting up and acquiring locks; all you have to do is specify which resources require locks.

8.3.1 Serializing Access to Methods

The most common need for synchronization among threads in Java is to serialize their access to some resource (an object) in other words, to make sure that only one thread at a time can manipulate an object or variable.[2] In Java, every object has a lock associated with it. To be more specific, every class and every instance of a class has its own lock. The synchronized keyword marks places where a thread must acquire the lock before proceeding.

For example, say we implemented a SpeechSynthesizer class that contains a say() method. We don't want multiple threads calling say() at the same time, or we wouldn't be able to understand anything being said. So we mark the say() method as synchronized, which means that a thread has to acquire the lock on the SpeechSynthesizer object before it can speak:

class SpeechSynthesizer {     synchronized void say( String words ) {         // speak     } }

Because say() is an instance method, a thread has to acquire the lock on the SpeechSynthesizer instance it is using before it can invoke the say() method. When say() has completed, it gives up the lock, which allows the next waiting thread to acquire the lock and run the method. Note that it doesn't matter whether the thread is owned by the SpeechSynthesizer itself or some other object; every thread has to acquire the same lock, that of the SpeechSynthesizer instance. If say() were a class (static) method instead of an instance method, we could still mark it as synchronized. But in this case because there is no instance object involved, the lock is on the class object itself.

Often, you want to synchronize multiple methods of the same class so that only one method modifies or examines parts of the class at a time. All static synchronized methods in a class use the same class object lock. By the same token, all instance methods in a class use the same instance object lock. In this way, Java can guarantee that only one of a set of synchronized methods is running at a time. For example, a SpreadSheet class might contain a number of instance variables that represent cell values, as well as some methods that manipulate the cells in a row:

class SpreadSheet {     int cellA1, cellA2, cellA3;        synchronized int sumRow( ) {         return cellA1 + cellA2 + cellA3;     }        synchronized void setRow( int a1, int a2, int a3 ) {         cellA1 = a1;         cellA2 = a2;         cellA3 = a3;     }     ... }

In this example, both methods setRow() and sumRow() access the cell values. You can see that problems might arise if one thread were changing the values of the variables in setRow() at the same moment another thread was reading the values in sumRow(). To prevent this, we have marked both methods as synchronized. When threads are synchronized, only one is run at a time. If a thread is in the middle of executing setRow() when another thread calls sumRow(), the second thread waits until the first one is done executing setRow() before it gets to run sumRow(). This synchronization allows us to preserve the consistency of the SpreadSheet. And the best part is that all this locking and waiting is handled by Java; it's transparent to the programmer.

In addition to synchronizing entire methods, the synchronized keyword can be used in a special construct to guard arbitrary blocks of code. In this form it also takes an explicit argument that specifies the object for which it is to acquire a lock:

synchronized ( myObject ) {     // Functionality that needs to be synced }

This code block can appear in any method. When it is reached, the thread has to acquire the lock on myObject before proceeding. In this way, we can synchronize methods (or parts of methods) in different classes in the same way as methods in the same class.

A synchronized instance method is, therefore, equivalent to a method with its statements synchronized on the current object. Thus:

synchronized void myMethod ( ) {     ... }

is equivalent to:

void myMethod ( ) {     synchronized ( this ) {         ...     }  } 
8.3.1.1 Accessing instance variables

In the SpreadSheet example, we guarded access to a set of instance variables with a synchronized method, which we did mainly so that we wouldn't change one of the variables while someone was reading the rest of them. We wanted to keep them coordinated. But what about individual variable types? Do they need to be synchronized? Normally the answer is no. Almost all operations on primitives and object reference types in Java happen atomically: they are handled by the virtual machine in one step, with no opportunity for two threads to collide. You can't be in the middle of changing a reference and be only part way done when another thread looks at the reference.

But watch out we did say almost. If you read the Java virtual machine specification carefully, you will see that the double and long primitive types are not guaranteed to be handled atomically. Both of these types represent 64-bit values. The problem has to do with how the Java VM's stack handles them. It is possible that this specification will be beefed up in the future. But for now, if you have any fears, synchronize access to your double and long instance variables through accessor methods.

8.3.1.2 Reentrant locking

The locks acquired by Java upon entering a synchronized method or block of code are reentrant. This means that the thread holding onto the lock may acquire the same lock again any number of times and will never block waiting for itself. In most cases this just means that the code behaves as you'd expect; a thread can call a synchronized method recursively, for example, and can itself call upon other synchronized methods within the same object.

8.3.2 The wait( ) and notify( ) Methods

With the synchronized keyword, we can serialize the execution of complete methods and blocks of code. The wait() and notify() methods of the Object class extend this capability. Every object in Java is a subclass of Object, so every object inherits these methods. By using wait() and notify(), a thread can effectively give up its hold on a lock at an arbitrary point and then wait for another thread to give it back before continuing.[3] All of the coordinated activity still happens inside synchronized blocks, and still only one thread is executing at a given time.

By executing wait() from a synchronized block, a thread gives up its hold on the lock and goes to sleep. A thread might do this if it needs to wait for something to happen in another part of the application, as we'll see shortly. Later, when the necessary event happens, the thread that is running it calls notify() from a block synchronized on the same object. Now the first thread wakes up and begins trying to acquire the lock again.

When the first thread manages to reacquire the lock, it continues from the point it left off. However, the thread that waited may not get the lock immediately (or perhaps ever). It depends on when the second thread eventually releases the lock and which thread manages to snag it next. Note also that the first thread won't wake up from the wait() unless another thread calls notify(). There is an overloaded version of wait(), however, that allows us to specify a timeout period. If another thread doesn't call notify() in the specified period, the waiting thread automatically wakes up.

Let's look at a simple scenario to see what's going on. In the following example, we'll assume there are three threads one waiting to execute each of the three synchronized methods of the MyThing class. We'll call them the waiter, notifier, and related threads. Here's a code fragment to illustrate:

class MyThing {     synchronized void waiterMethod( ) {         // do some stuff         wait( );   // now wait for notifier to do something         // continue where we left off     }     synchronized void notifierMethod( ) {         // do some stuff         notify( );  // notify waiter that we've done it         // continue doing stuff     }     synchronized void relatedMethod( ) {         // do some related stuff     }     ... }

Let's assume waiter gets through the gate first and begins executing waiter-Method( ). The two other threads are initially blocked, trying to acquire the lock for the MyThing object. When waiter executes the wait() method, it relinquishes its hold on the lock and goes to sleep. Now there are now two viable threads waiting for the lock. Which thread gets it depends on several factors, including chance and the priorities of the threads. (We'll discuss thread scheduling in the next section.)

Let's say that notifier is the next thread to acquire the lock, so it begins to run notifierMethod(). waiter continues to sleep, and related languishes, waiting for its turn. When notifier executes the call to notify(), the runtime system prods the waiter thread, effectively telling it something has changed. waiter then wakes up and rejoins related in vying for the MyThing lock. Note that it doesn't receive the lock automatically; it just changes from saying, "Leave me alone" to "I want the lock."

At this point, notifier still owns the lock and continues to hold it until the synchronized notifierMethod() returns or perhaps executes a wait() itself. At that point, the other two methods get to fight over the lock. waiter would like to continue executing waiterMethod() from the point it left off, while related, which has been patient, would like to get started. We'll let you choose your own ending for the story.

For each call to notify(), the runtime system wakes up just one method that is asleep in a wait() call. If there are multiple threads waiting, Java picks a thread on an arbitrary basis, which may be implementation-dependent. The Object class also provides a notifyAll() call to wake up all waiting threads. In most cases, you'll probably want to use notifyAll() rather than notify(). Keep in mind that notify() really means, "Hey, something related to this object has changed. The condition you are waiting for may have changed, so check it again." In general, there is no reason to assume only one thread at a time is interested in the change or able to act upon it. Different threads might look upon whatever has changed in different ways.

Often, our waiter thread is waiting for a particular condition to change, and we will want it to sit in a loop like the following:

while ( condition != true )      wait( );

Other synchronized threads call notify() or notifyAll() when they have modified the environment so that waiter can check the condition again. Using wait conditions like this is the civilized alternative to polling and sleeping, as you'll see in the following section.

8.3.3 Passing Messages

Now we'll illustrate a classic interaction between two threads: a Producer and a Consumer. A producer thread creates messages and places them into a queue while a consumer reads and displays them. To be realistic, we'll give the queue a maximum depth. And to make things really interesting, we'll have our consumer thread be lazy and run much more slowly than the producer. This means that Producer occasionally has to stop and wait for Consumer to catch up. Here are the Producer and Consumer classes:

import java.util.*;    public class Consumer implements Runnable {     Producer producer;        Consumer( Producer producer ) {         this.producer = producer;     }        public void run(  ) {         while ( true ) {             String message = producer.getMessage(  );             System.out.println("Got message: " + message);             try {                  Thread.sleep( 2000 );              } catch ( InterruptedException e ) { }         }     }        public static void main(String args[]) {         Producer producer = new Producer(  );         new Thread( producer ).start(  );         Consumer consumer = new Consumer( producer );         new Thread( consumer ).start(  );     } }    public class Producer implements Runnable{     static final int MAXQUEUE = 5;     private List messages = new ArrayList(  );        public void run(  ) {         while ( true ) {             putMessage(  );             try {                  Thread.sleep( 1000 );              } catch ( InterruptedException e ) { }         }     }        private synchronized void putMessage(  )      {         while ( messages.size(  ) >= MAXQUEUE )             try {                 wait(  );             } catch( InterruptedException e ) { }            messages.add( new java.util.Date().toString(  ) );         notify(  );     }        // called by Consumer     public synchronized String getMessage(  )     {         while ( messages.size(  ) == 0 )             try {                 notify(  );                 wait(  );             } catch( InterruptedException e ) { }         String message = (String)messages.remove(0);         notify(  );         return message;     } } 

For convenience, we have included a main() method in the Consumer class that runs the complete example. It creates a Consumer that is tied to a Producer and starts the two classes. You can run the example as follows:

% java Consumer

This produces the timestamp messages created by the Producer:

Got message: Sun Dec 19 03:35:55 CST 1999  Got message: Sun Dec 19 03:35:56 CST 1999    Got message: Sun Dec 19 03:35:57 CST 1999    ...

The timestamps initially show a spacing of one second, although they appear every two seconds. Our Producer runs faster than our Consumer. Producer would like to generate a new message every second, while Consumer gets around to reading and displaying a message only every two seconds. Can you see how long it will take the message queue to fill up? What will happen when it does?

Let's look at the code. We are using a few new tools here. Producer and Consumer implement the Runnable interface, and each has a thread associated with it. The Producer and Consumer classes pass messages through an instance of a java.util.List object. We haven't discussed the List class yet. Think of this one as a queue: we simply add and remove elements in first-in, first-out order.

The important activity is in the synchronized methods: putMessage() and getMessage(). Although one of the methods is used by the Producer thread and the other by the Consumer thread, they both live in the Producer class so that we can coordinate them simply by declaring them synchronized. Here they both implicitly use the Producer object's lock. If the queue is empty, the Consumer blocks in a call in the Producer, waiting for another message.

Another design option would implement the getMessage() method in the Consumer class and use a synchronized code block to synchronize explicitly on the Producer object. In either case, synchronizing on the Producer enables us to have multiple Consumer objects that feed on the same Producer. We'll do that later in this section.

putMessage()'s job is to add a new message to the queue. It can't do this if the queue is already full, so it first checks the number of elements in messages. If there is room, it stuffs in another timestamp message. If the queue is at its limit, however, putMessage() has to wait until there's space. In this situation, putMessage() executes a wait() and relies on the consumer to call notify() to wake it up after a message has been read. Here we have putMessage() testing the condition in a loop. In this simple example, the test probably isn't necessary; we could assume that when putMessage() wakes up, there is a free spot. However, this test is another example of good programming practice. Before it finishes, putMessage() calls notify() itself to prod any Consumer that might be waiting on an empty queue.

getMessage() retrieves a message for the Consumer. It enters a loop like that of putMessage(), waiting for the queue to have at least one element before proceeding. If the queue is empty, it executes a wait() and expects the Producer to call notify() when more items are available. Notice that getMessage() makes its own calls to notify(). It does this any time the queue is empty, to prod a producer that might be sleeping and also after it consumes a message, to give the producer the go ahead to fill the queue again. These scenarios are more plausible if there are more consumers, as we'll see next.

Now let's add another consumer to the scenario, just to make things really interesting. Most of the necessary changes are in the Consumer class; here's the code for the modified class, now called NamedConsumer:

public class NamedConsumer implements Runnable  {     Producer producer;     String name;        NamedConsumer(String name, Producer producer) {         this.producer = producer;         this.name = name;     }        public void run(  ) {         while ( true ) {             String message = producer.getMessage(  );             System.out.println(name + " got message: " + message);             try {                  Thread.sleep( 2000 );              } catch ( InterruptedException e ) { }         }     }        public static void main(String args[]) {         Producer producer = new Producer(  );         new Thread( producer ).start(  );            NamedConsumer consumer = new NamedConsumer( "One", producer );         new Thread( consumer ).start(  );         consumer = new NamedConsumer( "Two", producer );         new Thread( consumer ).start(  );     } }

The NamedConsumer constructor takes a string name to identify each consumer. The run() method uses this name in the call to println() to identify which consumer received the message.

The only required modification to the Producer code is to change the notify() calls to notifyAll() calls in putMessage() and getMessage(). (We could have used notifyAll() in the first place.) Now, instead of the consumer and producer playing tag with the queue, we can have many players waiting for the condition of the queue to change. We might have a number of consumers waiting for a message, or we might have the producer waiting for a consumer to take a message. Whenever the condition of the queue changes, we prod all the waiting methods to reevaluate the situation by calling notifyAll().

Here is some sample output when there are two NamedConsumers running, as in the main() method shown previously:

One got message: Sat Mar 20 20:00:01 CST 1999    Two got message: Sat Mar 20 20:00:02 CST 1999    One got message: Sat Mar 20 20:00:03 CST 1999    Two got message: Sat Mar 20 20:00:04 CST 1999    One got message: Sat Mar 20 20:00:05 CST 1999    Two got message: Sat Mar 20 20:00:06 CST 1999    One got message: Sat Mar 20 20:00:07 CST 1999    Two got message: Sat Mar 20 20:00:08 CST 1999    ...

We see nice, orderly alternation between the two consumers as a result of the calls to sleep() in the various methods. Interesting things would happen, however, if we were to remove all calls to sleep() and let things run at full speed. The threads would compete, and their behavior would depend on whether the system is using time-slicing. On a time-sliced system, there should be a fairly random distribution between the two consumers while on a nontime-sliced system, a single consumer could monopolize the messages. And since you're probably wondering about time-slicing, let's talk about thread priority and scheduling.

8.3.4 ThreadLocal Objects

A common issue that arises is the need to maintain some information or state on a per-thread basis. For example, we might want to carry some context with the current thread as it executes our application. Or we might simply want to have a value that is different for different threads. Java supports this through the ThreadLocal class. A ThreadLocal is an object wrapper that automatically maintains a separate value for any thread calling it. For example:

ThreadLocal userID = new ThreadLocal(  ); userID.set("Pat");  // called by thread 1 userID.set("Bob"); // called by thread 2 userID.get(  ); // thread 1 gets "Pat" userID.get(  ); // thread 2 gets "Bob"

You can use an instance of ThreadLocal anywhere you might use a static or instance variable to automatically maintain separate values for each thread.

8.4 Scheduling and Priority

Java makes few guarantees about how it schedules threads. Almost all of Java's thread scheduling is left up to the Java implementation and, to some degree, the application. Although it might have made sense (and would certainly have made many developers happier) if Java's developers had specified a scheduling algorithm, a single scheduling algorithm isn't necessarily suitable for all the roles that Java can play. Instead, Sun decided to put the burden on you to write robust code that works whatever the scheduling algorithm, and let the implementation tune the algorithm for whatever is best.[4]

Therefore, the priority rules that we'll describe next are carefully worded in the Java language specification to be a general guideline for thread scheduling. You should be able to rely on this behavior overall (statistically), but it is not a good idea to write code that relies on very specific features of the scheduler to work properly. You should instead use the control and synchronization tools that we have described in this chapter to coordinate your threads.[5]

Every thread has a priority value. If at any time a thread of a higher priority than the current thread becomes runnable (is started, stops sleeping, or is notified), it preempts the lower-priority thread and begins executing. By default, threads at the same priority are scheduled round-robin, which means once a thread starts to run, it continues until it does one of the following:

  • Sleeps, by calling Thread.sleep() or wait()

  • Waits for a lock, in order to run a synchronized method

  • Blocks on I/O, for example, in a read() or accept() call

  • Explicitly yields control, by calling yield()

  • Terminates, by completing its target method or with a stop() call (deprecated)

This situation looks something like Figure 8-4.

Figure 8-4. Priority preemptive, round-robin scheduling

figs/lj2.0804.gif

8.4.1 Time-Slicing

In addition to prioritization, many systems implement time-slicing of threads.[6] In a time-sliced system, thread processing is chopped up, so that each thread runs for a short period of time before the context is switched to the next thread, as shown in Figure 8-5.

Figure 8-5. Priority preemptive, time-sliced scheduling

figs/lj2.0805.gif

Higher-priority threads still preempt lower-priority threads in this scheme. The addition of time-slicing mixes up the processing among threads of the same priority; on a multiprocessor machine, threads may even be run simultaneously. This can introduce a difference in behavior for applications that don't use threads and synchronization properly.

Since Java doesn't guarantee time-slicing, you shouldn't write code that relies on this type of scheduling; any software you write needs to function under the default round-robin scheduling. If you're wondering what your particular flavor of Java does, try the following experiment:

public class Thready {     public static void main( String args [] ) {         new ShowThread("Foo").start(  );         new ShowThread("Bar").start(  );     }        static class ShowThread extends Thread {         String message;            ShowThread( String message ) {             this.message = message;         }         public void run(  ) {             while ( true )                 System.out.println( message );         }     } }

The Thready class starts up two ShowThread objects. ShowThread is a thread that goes into a hard loop (very bad form) and prints its message. Since we don't specify a priority for either thread, they both inherit the priority of their creator, so they have the same priority. When you run this example, you will see how your Java implementation does its scheduling. Under a round-robin scheme, only "Foo" should be printed; "Bar" never appears. In a time-slicing implementation, you should occasionally see the "Foo" and "Bar" messages alternate (which is most likely what you will see).

8.4.2 Priorities

Now let's change the priority of the second thread:

class Thready {     public static void main( String args [] ) {         new ShowThread("Foo").start( );         Thread bar = new ShowThread("Bar");         bar.setPriority( Thread.NORM_PRIORITY + 1 );         bar.start( );     } }

As you might expect, this changes how our example behaves. Now you may see a few "Foo" messages, but "Bar" should quickly take over and not relinquish control, regardless of the scheduling policy.

Here we have used the setPriority() method of the Thread class to adjust our thread's priority. The Thread class defines three standard priority values (they're integers): MIN_PRIORITY, NORM_PRIORITY, and MAX_PRIORITY.

If you need to change the priority of a thread, you should use one of these values, possibly with a small increment or decrement. Avoid using values near MAX_PRIORITY; if you elevate many threads to this priority level, priority will quickly become meaningless. A slight increase in priority should be enough for most needs. For example, specifying NORM_PRIORITY + 1 in our example is enough to beat out our other thread.

We should also note that in an applet environment you may not have access to maximum priority because you're limited by the maximum priority of the thread group in which you were created (see "Thread Groups" later in this chapter).

Finally, in our opinion, utilizing thread priorities should really be reserved more for system and framework development. It is not as useful or flexible in practice as simply implementing application-level control over processing using patterns like prioritized queues or pools.

8.4.3 User-Controlled Time-Slicing

There is a rough technique you can use to achieve an effect similar to time-slicing in a Java application, even if the Java runtime system does not support it directly. The idea is simply to create a high (maximum) priority thread that does nothing but repeatedly sleep for a short interval and then wake up. Since the higher-priority thread (in general) interrupts any lower-priority threads when it becomes runnable, you effectively chop up the execution time of your lower-priority threads, which should then execute in the standard round-robin fashion. We call this technique rough because of the weakness of the specification for Java threads with respect to their preemptiveness. If you use this technique, you should consider it only a potential optimization.

8.4.4 Yielding

Whenever a thread sleeps, waits, or blocks on I/O, it gives up its time slot, and another thread is scheduled. As long as you don't write methods that use hard loops, all threads should get their due. However, a thread can also signal that it is willing to give up its time voluntarily at any point with the yield() call. We can change our previous example to include a yield() on each iteration:

... static class ShowThread extends Thread {     ...     public void run( ) {         while ( true ) {             System.out.println( message );             yield( );         }     } }

Now you should see "Foo" and "Bar" messages strictly alternating. If you have threads that perform very intensive calculations or otherwise eat a lot of CPU time, you might want to find an appropriate place for them to yield control occasionally. Alternatively, you might want to drop the priority of your compute-intensive thread so that more important processing can proceed around it.

Unfortunately the Java language specification is very weak with respect to yield(). It is another one of these things you should consider an optimization hint rather than a guarantee. In the worst case, the runtime system may simply ignore calls to yield().

8.4.5 Native Threads

We mentioned the possibility that different threads could run on different processors. This would be an ideal Java implementation. Unfortunately, many implementations don't even allow multiple threads to run in parallel with other processes running on the same machine. Older implementations of threading (known variously as "pthreads" or "green threads") effectively simulate threading within an individual process like the Java interpreter. One feature that you might want to confirm in choosing a Java implementation is called native threads. This means that the Java runtime system is able to use the real (native) threading mechanism of the host environment, which should perform better and, ideally, allow multiprocessor operation.

8.5 Thread Groups

The ThreadGroup class allows us to deal with threads wholesale: we can use it to arrange threads in groups and deal with the groups as a whole. A thread group can contain other thread groups, in addition to individual threads, so our arrangements can be hierarchical. Thread groups are particularly useful when we want to start a task that might create many threads of its own. By assigning the task a thread group, we can later identify and control all the task's threads. Thread groups are also the subject of restrictions that can be imposed by the Java Security Manager. So we can restrict a thread's behavior according to its thread group. For example, we can forbid threads in a particular group from interacting with threads in other groups. This is one way web browsers can prevent threads started by Java applets from stopping important system threads.

When we create a thread, it normally becomes part of the thread group to which the currently running thread belongs. To create a new thread group of our own, we can call the constructor:

ThreadGroup myTaskGroup = new ThreadGroup("My Task Group");

The ThreadGroup constructor takes a name, which a debugger can use to help you identify the group. (You can also assign names to the threads themselves.) Once we have a group, we can put threads in the group by supplying the ThreadGroup object as an argument to the Thread constructor:

Thread myTask = new Thread( myTaskGroup, taskPerformer );

Here, myTaskGroup is the thread group, and taskPerformer is the target object (the Runnable object that performs the task). Any additional threads that myTask creates also belong to the myTaskGroup thread group.

8.5.1 Working with the ThreadGroup Class

Creating thread groups isn't interesting unless you do things with them. The ThreadGroup class exists so that you can control threads in batches. It has methods that parallel the basic Thread control methods even the deprecated stop(), suspend(), and resume(). These methods in the thread group operate on all the threads they contain. You can also mark a thread group as a "daemon"; a daemon thread group is automatically removed when all its children are gone. If a thread group isn't a daemon, you have to call destroy() to remove it when it is empty.

We can set the maximum priority for threads created in a thread group by calling setMaximumPriority(). Thereafter, no threads can be created in the thread group with a priority higher than the maximum; threads that change their priority can't set their new priority higher than the maximum.

Finally, you can get a list of all threads in a group. The method activeCount() tells you how many threads are in the group; the method enumerate() gives you a list of them. The argument to enumerate() is an array of Threads, which enumerate() fills in with the group's threads. (Use activeCount() to make an array of the right size.) Both activeCount() and enumerate() operate recursively on all thread groups the group contains.

It is also the responsibility of the ThreadGroup to handle uncaught runtime exceptions thrown by the run() methods of its threads. You can override the uncaughtException() method of ThreadGroup when making your own thread groups to control this behavior.

8.6 Thread Performance

The way that applications use threads and the associated costs and benefits have greatly impacted the design of many Java APIs. We will discuss some of the issues in detail in other chapters of this book. But it is worth mentioning briefly here some aspects of thread performance and how the use of threads has dictated the form and functionality of several recent Java packages.

8.6.1 The Cost of Synchronization

The act of acquiring locks to synchronize threads, even when there is no contention, takes time. In older implementations of Java this time could be significant. With newer VMs, it is almost negligible.[7] However, unnecessary synchronization at a low level can still slow applications where legitimate concurrent access would be proper and could be more efficiently organized at a higher level of abstraction. Because of this, two important APIs were specifically crafted to avoid unnecessary synchronization, by placing it under the control of the developer: the Java Collections API and the Swing GUI API.

The java.util Collections API replaces earlier simple Java aggregate types namely Vector and Hashtable with more fully featured and, notably, unsynchronized types (List and HashMap). The Collections API allows application code to synchronize access to collections and provides special "fail fast" functionality to help detect concurrent access and throw an exception. It also provides synchronization "wrappers" that can provide safe access in the old style.

The Java Swing GUI, which grew out of AWT, has taken a different very approach to providing speed and safety. Swing dictates that modification of its components (with notable exceptions) must all be done by a single thread: the main event queue. Swing solves performance problems as well as nasty issues of determinism in ordering of events by forcing a single super-thread to control the GUI. The application may access the event queue thread indirectly by pushing commands onto a queue through a simple interface.

8.6.2 Thread Resource Consumption

A fundamental pattern in Java, which we will see illustrated in Chapter 11 and Chapter 12, is to start many threads to handle asynchronous external resources such as socket connections. For maximum efficiency, a web server might, for example, be tempted to create a thread for each client connection it is servicing. With each client having its own thread, I/O operations may block and restart as needed. But as efficient as this may be in terms of throughput, it is very inefficient in terms of server resources. Threads consume memory; each thread has its own "stack" for local variables, and switching between running threads (context switching) adds overhead to the CPU. While threads are relatively lightweight (in theory it is possible to have hundreds or perhaps thousands running on a large server) at a certain point the resources consumed by the threads themselves start defeating the purpose of starting more threads. Often this point is reached at only a few dozen threads. Creating a thread per client is not a very scaleable option.

An alternative approach is to create "thread pools" where a fixed number of threads pull tasks from a queue and return for more when they are finished. This recycling of threads makes for solid scalability, but it has historically been difficult to implement efficiently in Java because stream I/O (for things like sockets) has not fully supported nonblocking operations. This has changed with Java 1.4 and the introduction of the NIO (new I/O) package, java.nio. The NIO package introduces asynchronous I/O channels: nonblocking reads and writes along with the ability to "select" or test the readiness of streams for moving data. Channels can also be asynchronously closed, allowing threads to work gracefully. With the NIO package, it should be possible to create servers with much more sophisticated, scaleable thread patterns.

[1]  interrupt() does not work in versions of Java prior to 1.1 and, historically, has not worked consistently in all Java implementations.

[2]  Don't confuse the term "serialize" in this context with Java object serialization, which is a mechanism for making objects persistent. But the underlying meaning (to place one thing after another) does apply to both. In the case of object serialization, it is the object's data which is laid out, byte for byte, in a certain order.

[3]  In actuality, they don't really pass the lock around; the lock becomes available and, as we'll describe, a thread that is scheduled to run acquires it.

[4]  A notable alternative to this is the "real-time" Java specification which defines specialized thread behavior for certain types of applications. It is being developed under the Java community process and can be found at http://www.rtj.org/.

[5]   Java Threads by Scott Oaks and Henry Wong (O'Reilly) includes a detailed discussion of synchronization, scheduling, and other thread-related issues.

[6]  In the beginning, with Java's Release 1.0, Sun's Interpreter for Windows used time-slicing, as did the Netscape Navigator Java VM. Sun's Java 1.0 for Solaris didn't. All modern implementations using "real threads" should perform time-slicing, so this question is largely settled.

[7]  In a completely na ve test (simple loop) using JDK 1.4.0 on a 400-MHz Sparc Ultra-60, we measured the cost of synchronization on an object to be about one-tenth of a microsecond. However, when the lock is contested it would surely be more expensive.

CONTENTS


Learning Java
Learning Java, Second Edition
ISBN: 0596002858
EAN: 2147483647
Year: 2002
Pages: 30

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net