Contents:
Introducing Threads
Threading
Applets
Synchronization
Scheduling
and Priority
Threads have been around for some time, but few programmers have
actually worked with them. There is even some debate over whether or not the
average programmer can use threads effectively. In Java, working with threads
can be easy and productive. In fact, threads provide the only way to effectively
handle a number of tasks. So it's important that you become familiar with
threads early in your exploration of Java.
Threads are integral to the way Java works. We've already seen
that an applet's paint()
method isn't called by the applet itself, but by another thread within the
interpreter. 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 a half dozen or more threads running in an applet without even trying,
simply by requesting 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.
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 can be used by many players at the same time. Sharing the same address
space means that threads share instance variables, but not local variables,
just like players share the golf course, but not personal things like clubs and
balls.
Multiple threads in an application have the same problems as the
players sharing a golf course: 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, or course, and those details have
historically made threads difficult to use. Java makes creating, controlling,
and coordinating threads simple. When creating a new thread is the best way to
accomplish some task, it should be as easy as adding a new component to your
application.
It is common to stumble over threads when you first look at 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.
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 synchronizing its execution. With it, we can start the
thread, stop the thread, or suspend it temporarily. 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, we can't specify one directly.
Instead, we have to take a short detour and use the Runnable interface to create an
object that contains a "runnable" method.
An object that wants to serve as the target of a Thread can
declare that it has an appropriate executable method by implementing the java.lang.Runnable
interface. Runnable
defines a single, general-purpose method:
public interface Runnable {
abstract public void run();
}
Every thread begins its life by executing a run() method in
a particular object. run()
is a rather mundane method that can hold an arbitrary body of code. It is public, takes no
arguments, has no return value, and is not allowed to throw any exceptions.
Any class can contain an appropriate run() method, simply by declaring
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.
In this way, we can effectively run a method in any object we want.
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 completes, or we call the
thread's stop()
method to kill the thread permanently. A little later, we will look at some
other methods you can use to control the 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
...
repaint();
}
}
}
To use it, we create a Thread object with an instance of Animation as its
target object, and invoke its start() method. We can perform these steps
explicitly, as in the following:
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!
The above situation is not terribly object oriented. More often,
we want an object to handle its own thread, as shown in Figure
6.1.
Figure
6.1 depicts a Runnable
object that creates and starts its own Thread. We can have our Animation class
perform these actions in its constructor:
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 instance. We keep the Thread reference in the instance
variable myThread,
in case we want to stop the show, or exercise some other kind of control.
The Runnable
interface lets us make an arbitrary object the target of a thread, as we did
above. This is the most important, general usage of the Thread class. In
most situations where you need to use threads, you'll create a class that
implements the Runnable
interface. I'd be remiss, however, if I didn't 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. The Thread class itself implements the Runnable
interface; it has its own run()
method we can override to make it do something useful:
class Animation extends Thread {
...
public void run() {
while (true ) {
// Draw Frames
...
repaint();
}
}
}
The skeleton of our Animation class above looks much the same as before,
except that our class is now a kind of Thread. To go along with this scheme,
the default (empty) 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
6.2. Note that our subclass must override the run() method in the Thread class
because Thread
simply defines an empty run()
method.
Now we create an instance of Animation and call its start() method:
Animation bouncy = new Animation("Bouncy");
bouncy.start();
Alternatively, we can have the Animation object start itself 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 it is created.
Subclassing Thread
probably seems like a convenient way to bundle a Thread and its target run() method.
However, as always, you should let good object-oriented design dictate how you
structure your classes. In most cases, a specific run() method is probably closely
related to the functionality of a particular class in your application, so you
should implement run()
in that class. This technique has the added advantage of allowing run() to access
any private
variables and methods it might need in the class.
If you subclass Thread to implement a thread, you are saying you
need a new type of object that is a kind of Thread. While there is something
unnaturally satisfying about making an object primarily concerned with
performing a single task (like animation), the actual situations where you'll
want to create a subclass of Thread should be rather rare. If you find you're
subclassing Thread
left and right, you may want to examine whether you are falling into the design
trap of making objects that are simply glorified functions.
We have seen the start() method used to bring a newly created Thread to life.
Three other methods let us control a Thread's execution: stop(), suspend(), and resume(). None
of these methods take any arguments; they all operate on the current thread
object. The stop()
method complements start();
it destroys the thread. start()
and stop()
can be called only once in the life of a Thread. By contrast, the suspend() and resume() methods
can be used to arbitrarily pause and then restart the execution of a Thread.
Somewhere mention stop(Throwable)
There is a form of Thread.stop that takes a Throwable as an argument
and throws that exception:
workingThread.stop(new CancelWhatYourDoingException());
Often, for simple tasks, it is easy enough to throw away a thread
when we want to stop it and simply create a new one when want to proceed again.
suspend()
and resume()
can be used in situations where the Thread's setup is very expensive. For
example, if creating the thread involves opening a socket and setting up some
elaborate communication, it probably makes more sense to use suspend() and resume() with
this thread.
Another common need is to put a thread to sleep for some period
of time. Thread.sleep()
is a static method of the Thread
class that causes the currently executing thread to delay for a specified
number of milliseconds:
try {
Thread.sleep ( 1000 );
}
catch ( InterruptedException e ) {
}
Thread.sleep()
throws an InterruptedException
if it is interrupted by another Thread.[1] When 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. We'll talk more about thread priority and
scheduling later.
[1] The Thread class
contains an interrupt()
method to allow one thread to interrupt another thread, but this functionality
is not implemented in Java 1.0.
A Thread
continues to execute until one of the following things happens:
So what happens if the run() method for a thread never
terminates, and the application that started the thread never calls its stop() method?
The answer is that the thread lives on, even after 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, what we really want is 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 of using daemon threads:
class Devil extends Thread {
Devil() {
setDaemon( true );
start();
}
public void run() {
// Perform evil tasks
...
}
}
In the above example, the Devil thread sets its daemon status when
it is created. If any Devil
threads remain when our application is otherwise complete, Java 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 system itself, but not in
applets. Since an applet runs inside of another Java application, any daemon
threads it creates will continue to live until the controlling application
exits--probably not the desired effect.
Applets are embeddable Java applications that are expected to be
able to start and stop themselves on command. Unlike threads, applets can be
started and stopped 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 scrolls the applet out of view. In general, we would like an
applet to cease its nonessential activity when it is stopped, and resume it
when started again. (See Chapter
10, Understand the Abstract Windowing Toolkit for a complete
discussion of applets.)
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. UpdateApplet
handles the basic starting and stopping behavior for us, as shown below.
public class UpdateApplet extends java.applet.Applet
implements Runnable {
private Thread updateThread;
int updateInterval = 1000;
public void run() {
while ( true ) {
try {
Thread.sleep( updateInterval );
}
catch (InterruptedException e ) { }
repaint();
}
}
public void start() {
if ( updateThread == null ) {
updateThread = new Thread(this);
updateThread.start();
}
}
public void stop() {
if ( updateThread != null ) {
updateThread.stop();
updateThread = null;
}
}
}
UpdateApplet
is a Runnable
object that alternately sleeps and calls its repaint() method. It has two other
public methods: start()
and stop().
These are methods of the Applet
class we are overriding; do not confuse them with the similarly named methods
of the Thread
class. These start()
and stop()
methods are called by the Java environment 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
kills 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
stopped, we kill the thread by invoking its stop() method and throw away the
reference by setting it to null.
Setting updateThread
to null
serves both to allow the garbage collector to clean up the dead Thread object,
and to indicate to UpdateApplet's
start()
method that the thread is gone.
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 of the work for us, we can now create the world's simplest clock
applet with just a few lines of code. Figure
6.3 shows our Clock.
(This might be a good one to run on your Java wrist watch.)
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()
sequence simply creates a string that contains the current time; we'll see
where this code comes from in Chapter
7, Basic Utility Classes.
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 of 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? One solution is to use Thread's suspend() and resume()
methods, as I'll show you momentarily.
Now if you've been wondering why we've been using stop() to kill
the thread, rather than using the suspend() and resume() methods all along, here's the
explanation you've been waiting for. The problem 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 use suspend() to suspend the applet. Now we
have no way of ensuring that the user will bring the applet back into view
before moving on to another page. And actually, the same situation would occur
if the user simply moves on to another page and never comes back.
If we call suspend(),
we'd really like to make sure we call resume() at a later date, or we'll end
up leaving the thread hanging in permanent suspense. But we have no way of
knowing if the applet will ever be restarted, so just putting a call to resume() in the
applet's start()
method won't work. Leaving the suspended thread around forever might not hurt
us, but it's not good programming practice to be wasteful. What we need is a
way to guarantee we can clean up our mess if the applet is never used again.
What to do?
There is a solution for this dilemma, but in many cases, like
with our simple Clock,
it's just easier to use stop(),
with a subsequent call to start()
if necessary. In cases where it is expensive to set up and tear down a thread,
we could make the following modifications to UpdateApplet:
public void start() {
if ( updateThread == null ) {
updateThread = new Thread(this);
updateThread.start();
}
else
updateThread.resume();
}
public void stop() {
updateThread.suspend();
public void destroy() {
if ( updateThread != null ) {
updateThread.stop();
updateThread = null;
}
}
These modifications change UpdateApplet so that it suspends and
restarts its updateThread,
rather than killing and recreating it. The new start() method creates the thread and
calls start()
if updateThread
is null;
otherwise it assumes that the thread has been suspended, so it calls resume(). The
applet's stop()
method simply suspends the thread by calling suspend().
What's new here is the destroy() method. This is another method
that UpdateApplet
inherits from the Applet
class. The method is called by the Java environment when the applet is going to
be removed (often from a cache). It provides a place where we can free up any
resources the applet is holding. This is the perfect place to cut the suspense
and clean up after our thread. In our destroy() method, we check to see that
the thread exists, and if it does, we call stop() to kill it and set its reference
to null.
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 they can work together and not collide in their use of the
same address space.
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 not unlike a public restroom at a gas station.
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, just as if 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.
The most common need for synchronization among threads in Java is
to serialize their access to some resource, namely an object. In other words,
synchronization makes sure only one thread at a time can perform certain
activities that manipulate an object. 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 particular 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 as there is no instance object involved,
the lock would be on the class object itself.
Often, you want to synchronize multiple methods of the same
class, so that only one of the methods 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 will be 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 of 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
...
}
The code block above 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 have methods (or parts of methods) in different classes synchronized the
same as methods in the same class.
A synchronized 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 ) {
...
}
}
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 give up
its hold on a lock at an arbitrary point, and then wait for another thread to
give it back before continuing. All of the coordinated activity still happens
inside of 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 you'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,
respectively. Here's a code fragment to illustrate:
class MyThing {
synchronized void waiterMethod() {
// Do some stuff
// Now we need to wait for notifier to do something
wait();
// Continue where we left off
}
synchronized void notifierMethod() {
// Do some stuff
// Notify waiter that we've done it
notify();
// Do more things
}
synchronized void relatedMethod() {
// Do some related stuff
}
Let's assume waiter
gets through the gate first and begins executing waiterMethod(). 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. waiter
continues to sleep and related
languishes, waiting for its turn. When notifier executes the call to notify(), Java
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 actually receive the lock; 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 it leaves its synchronized method (or perhaps executes a wait()
itself ). When it finally completes, the other two methods get to fight
over the lock. waiter
would like to continue executing waiterMethod() from the point it left off, while unrelated, 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(), Java wakes up just one method that is
asleep in a wait()
call. If there are multiple threads waiting, Java picks the first thread on a
first-in, first-out basis. 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 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. This is the
civilized alternative to polling and sleeping, as you'll see the following example.
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 them out 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 slower than the producer. This means that Producer
occasionally has to stop and wait for Consumer to catch up. The example below
shows the Producer
and Consumer
classes.
import java.util.Vector;
class Producer extends Thread {
static final int MAXQUEUE = 5;
private Vector messages = new Vector();
public void run() {
try {
while ( true ) {
putMessage();
sleep( 1000 );
}
}
catch( InterruptedException e ) { }
}
private synchronized void putMessage()
throws InterruptedException {
while ( messages.size() == MAXQUEUE )
wait();
messages.addElement( new java.util.Date().toString() );
notify();
}
// Called by Consumer
public synchronized String getMessage()
throws InterruptedException {
notify();
while ( messages.size() == 0 )
wait();
String message = (String)messages.firstElement();
messages.removeElement( message );
return message;
}
}
class Consumer extends Thread {
Producer producer;
Consumer(Producer p) {
producer = p;
}
public void run() {
try {
while ( true ) {
String message = producer.getMessage();
System.out.println("Got message: " + message);
sleep( 2000 );
}
}
catch( InterruptedException e ) { }
}
public static void main(String args[]) {
Producer producer = new Producer();
producer.start();
new Consumer( producer ).start();
}
}
For convenience, we have included a main() method that runs the
complete example in the Consumer
class. It creates a Consumer
that is tied to a Producer
and starts the two classes. You can run the example as follows:
% java Consumer
The output is the time-stamp messages created by the Producer:
Got message: Sun Dec 19 03:35:55 CST 1996
Got message: Sun Dec 19 03:35:56 CST 1996
Got message: Sun Dec 19 03:35:57 CST 1996
...
The time stamps 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 are
subclasses of Thread.
It would have been a better design decision to have Producer and Consumer
implement the Runnable
interface, but we took the slightly easier path and subclassed Thread. You
should find it fairly simple to use the other technique; you might try it as an
exercise.
The Producer
and Consumer
classes pass messages through an instance of a java.util.Vector object. We haven't
discussed the Vector
class yet, but you can think of this one as a queue where we add and remove
elements in first-in, first-out order. See Chapter
7, Basic Utility Classes for more information about the Vector class.
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 because they have to be synchronized on the same object to work together.
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 explicitly synchronize on the Producer object.
In either case, synchronizing on the Producer is important because it allows
us to have multiple Consumer
objects that feed on the same Producer.
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 time stamp. 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 the Producer's,
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 unconditional
call to notify().
This is a somewhat lazy way of keeping the Producer on its toes, so that the queue
should generally be full. Alternatively, getMessage() might test to see if the
queue had fallen below a low water mark before waking up the producer.
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;
the example below shows the code for the modified class.
class Consumer extends Thread {
Producer producer;
String name;
Consumer(String name, Producer producer) {
this.producer = producer;
this.name = name;
}
public void run() {
try {
while ( true ) {
String message = producer.getMessage();
System.out.println(name + " got message: " + message);
sleep( 2000 );
}
}
catch( InterruptedException e ) { }
}
public static void main(String args[]) {
Producer producer = new Producer();
producer.start();
// Start two this time
new Consumer( "One", producer ).start();
new Consumer( "Two", producer ).start();
}
}
The Consumer
constructor now 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 modification to make in the Producer code is to change the
call to notify()
in putMessage()
to a call to notifyAll().
Now, instead of the consumer and producer playing tag with the queue, we can
have many players waiting on 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 of the waiting methods to reevaluate the situation by
calling notifyAll().
Note, however, that we don't need to change the call to notify() in getMessage(). If
a Consumer
thread is waiting for a message to appear in the queue, it's not possible for
the Producer
to be simultaneously waiting because the queue is full.
Here is some sample output when there are two consumers running,
as in the main()
method shown above:
One got message: Wed Mar 20 20:00:01 CST 1996
Two got message: Wed Mar 20 20:00:02 CST 1996
One got message: Wed Mar 20 20:00:03 CST 1996
Two got message: Wed Mar 20 20:00:04 CST 1996
One got message: Wed Mar 20 20:00:05 CST 1996
Two got message: Wed Mar 20 20:00:06 CST 1996
One got message: Wed Mar 20 20:00:07 CST 1996
Two got message: Wed Mar 20 20:00:08 CST 1996
...
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 of the calls to sleep()
and let things run at full speed. The threads would compete and their behavior
would depend on whether or not 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.
Java makes certain guarantees as to how its threads are
scheduled. Every thread has a priority value. If, at any time, a thread of a
higher priority than the current thread becomes runnable, 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
Calls Thread.sleep() or wait()
Waits for lock
Waits for a lock in order to run a
synchronized method
Blocks on I/O
Blocks, for example, in a xread() or an accept() call
Explicitly yields control
Calls yield()
Terminates
Completes its target method or is
terminated by a stop()
call
This situation looks something like what's shown in Figure
6.4.
Java leaves certain aspects of scheduling up to the
implementation.[2] The main point here is that some, but not all,
implementations of Java use time slicing on threads of the same priority.[3] 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
6.5.
[2] This
implementation-dependent aspect of Java isn't a big deal, since it doesn't hurt
for an implementation to add time slicing on top of the default round-robin
scheduling. It's actually not hard to create a time-slicing effect by simply
having a high-priority thread sleeping for a specified time interval. Every
time it wakes up, it interrupts a lower-priority thread and causes processing
to shift round robin to the next thread.
[3] As of Java
Release 1.0, Sun's Java Interpreter for the Windows 95 and Windows NT platforms
uses time slicing, as does the Netscape Navigator Java environment. Sun's Java
1.0 for the Solaris UNIX platforms doesn't.
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. Unfortunately, this feature can lead to differences in your
application's behavior.
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. But if you're wondering what
your particular flavor of Java does, try the following experiment:
class Thready {
public static void main( String args [] ) {
new MyThread("Foo").start();
new MyThread("Bar").start();
}
}
class MyThread extends Thread {
String message;
MyThread ( String message ) {
this.message = message;
}
public void run() {
while ( true )
System.out.println( message );
}
}
The Thready
class starts up two MyThread
objects. Thready
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 it 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.
Now let's change the priority of the second thread:
class Thready {
public static void main( String args [] ) {
new MyThread("Foo").start();
Thread bar = new MyThread("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,
as shown in Table
6.1.
Table 6.1: Thread
Priority Values |
|
Value |
Definition |
MIN_PRIORITY |
Minimum priority |
NORM_PRIORITY |
Normal priority |
MAX_PRIORITY |
Maximum priority |
If you need to change the priority of a thread, you should use
one of these values or a close relative value. But let me warn you against
using MAX_PRIORITY
or a close relative value; 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.
As I said earlier, whenever a thread sleeps, waits, or blocks on
I/O, it gives up its time slot, and another thread is scheduled. So as long as
you don't write methods that use hard loops, all threads should get their due.
However, a Thread
can also give up its time voluntarily with the yield() call. We can change our previous
example to include a yield()
on each iteration:
class MyThread extends Thread {
...
public void run() {
while ( true ) {
System.out.println( message );
yield();
}
}
}
Now you should see "Foo" and "Bar" messages
alternating one for one. 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 intensive thread, so that more important
processing can proceed around it.