Threads have been around for some time, but few programmers have actually worked with them. There is even some debate over whether the average programmer can use threads effectively. In Java, working with threads can be easy and productive (at least for the most common cases). In fact, threads provide the only reasonable way to handle certain kinds 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. For example, an
applet’s paint( )
method isn’t called
by the applet itself, but rather by another 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 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 many golfers use 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 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. Java makes creating, controlling, and coordinating threads much simpler. 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 that 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
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 the
Runnable
object (the “target object”)
that was passed to the thread. The run( )
method
can contain any code, but it must be public, take no arguments, have
no return value, and throw no 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 call
any method it wants to after the thread is started.
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. 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( )
):
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.
The Runnable
interface lets us make an arbitrary
object the target of a thread, as we did earlier. 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.
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.)
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.)
Subclassing Thread
seems 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
. While there is something unnaturally
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 will be more natural to let the requirements of your program
dictate the class structure. 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.
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 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,
but at the expense of generating an extra class.
Note that we could be a bit more terse in the previous example by
simply having our anonymous inner class extend
Thread
rather than implement
Runnable
:
myThread = new Thread( ) { public void run() { drawFrames( ); } }; myThread.start( );
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 (if any) CPU time.The
interrupt( )
method wakes up a thread that is sleeping or is otherwise blocked on a long I/O operation.[25]The methods
wait()
andjoin( )
coordinate the execution of two or more threads. We’ll discuss them in detail when we talk about thread synchronization later in this chapter.
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, 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 make 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
of its internal locks used for thread synchronization. This can cause
unexpected behavior and, in the case of suspend( )
, can lead to deadlock situations.
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 using 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 to
nicely, rather than by pulling the rug out from under it
unexpectedly. The thread examples in this book will use this
technique in one way or another.
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 shouldn’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( )
. Either
way, the call causes the currently executing thread to delay for a
specified number of milliseconds:
try { // static convenience method Thread.sleep( 1000 ); // instance method sleep( 500 ); } catch ( InterruptedException e ) { // someone woke us up prematurely }
In
either case, sleep( )
throws an
InterruptedException
if it is interrupted by
another Thread via its 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.
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
dies. 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( )
.
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
in applets. Since an applet runs inside of another Java application,
any daemon threads it creates could 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 of the threads created
by subsystems of an application and then clean them up if necessary.
Get Learning Java now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.