Section 7.6
Timers, Animation, and Threads
JAVA IS A MULTI-THREADED LANGUAGE, which means that
several different things can be going on, in parallel. A thread
is the basic unit of program execution. A thread executes a sequence of instructions,
one after the other. When the system executes a stand-alone program, it creates a thread.
(Threads are usually called processes in this context, but
the differences are not important here.)
The commands of the program are executed sequentially, from beginning to end, by this
thread. The thread "dies" when the program ends. In a typical computer system,
many threads can exist at the same time. At a given time, only one thread can
actually be running, since the computer's Central Processing Unit (CPU) can
only do one thing at a time. (An exception to this is a multi-processing computer,
which has several CPUs. At a given time, every CPU can be executing a different thread.)
However, the computer uses time sharing to give the
illusion that several threads are being executed at the same time, "in parallel."
Time sharing means that the CPU executes one thread for a while,
then switches to another thread, then to another..., and then back to the first thread --
typically about 100 times per second.
As far as users are concerned, the threads might as well be running at the same time.
To say that Java is a multi-threaded language means that a Java program can create
one or more threads which will then run in parallel with the program. This is a fundamental,
built-in part of the language, not an option or add-on like it is in some languages.
Still, programming with threads can be tricky, and should be avoided unless it is really
necessary. Ideally, even then, you can avoid using threads directly by using well-tested
classes and libraries that someone has written for you.
Animation In Swing
One of the places where threads are used in GUI programming is to do animation.
An animation is just a sequence of still images displayed on the screen one
after the other. If the images are displayed quickly enough and if the changes from
one image to the next are small enough, then the viewer will perceive continuous motion.
To program an animation, you need some way of displaying a sequence of images.
A GUI program already has at least one thread, an event-handling
thread, which detects actions taken by the user and calls appropriate subroutines
in the program to handle each event. An animation, however, is not driven by user actions.
It is something that happens by itself, as an independent process. In Java, the most
natural way to accomplish this is to create a separate thread to run the animation.
Before the introduction of the Swing GUI, a Java programmer would have to deal with
this thread directly. This made animation much more complicated than it should
have been. The good news in Swing is that it is no longer necessary to program
directly with threads in order to do simple animations. The neat idea in Swing
is to integrate animation with event-handling, so that you can program animations
using the same techniques that are used for the rest of the program.
In Swing, an animation can be programmed using an object belonging to the
class javax.swing.Timer. A Timer object can generate a sequence
of events on its own, without any action on the part of the user. To program
an animation, all your program has to do is create a Timer and
respond to each event from the timer by displaying another frame in the animation.
Behind the scenes, the Timer runs a separate thread which is responsible for
generating the events, but you never need to deal with the thread directly.
The events generated by a Timer are of type ActionEvent.
The constructor for a Timer specifies two things: The amount of time
between events and an ActionListener that will be notified
of each event:
Timer(int delayTime, ActionListener listener)
The listener should be programmed to respond to events from
the Timer in its actionPerformed() method.
The delay time between events is specified in milliseconds (where
one second equals 1000 milliseconds). The actual delay time between
two events can be longer than the requested delay time, depending on
how long it takes to process the events and how busy the computer is
with other things. In a typical animation, somewhere between ten and
thirty frames should be displayed every second. These rates correspond
to delay times between 100 and 33.
A Timer does not start running automatically when it is
created. To make it run, you must call its start() method.
A timer also has a stop() method, which you can call to
make it stop generating events. If you have stopped a timer and
want to start it up again, you can call its restart() method.
(The start() method should be called only once.) None of
these methods have parameters.
Let's look at an example. In the following applet, you can start
an animation running by clicking the "Start" button. When you do this,
the text on the button changes to "Stop", and you can stop the animation
by clicking the button again. This is yet another applet that says
"Hello World." The animation simply cycles the color
of the message through all possible hues:
Here is part of the source code for this applet, omitting the
definition of the nested class that defines the drawing surface:
public class HelloWorldSpectrum extends JApplet {
Display display; // A JPanel belonging to a nested "Display"
// class; used for displaying "Hello World."
// It defines a method "setColor(Color)" for
// setting the color of the displayed message.
JButton startStopButton; // The button that will be used to
// start and stop the animation.
Timer timer; // The timer that drives the animation. A timer
// is started when the user starts the animation.
// Each time an ActionEvent is received from the
// timer, the color of the message will change.
// The value of this variable is null when the
// animation is not in progress.
int colorIndex; // This is be a number between 0 and 100 that
// will be used to determine the color. It will
// increase by 1 each time a timer event is
// processed.
public void init() {
// This is called by the system to initialize the applet.
// It adds a button to the "south" position in the applet's
// content pane, and it adds a display panel to the "center"
// position so that it will fill the rest of the content pane.
display = new Display();
// The component that displays "Hello World".
getContentPane().add(display, BorderLayout.CENTER);
// Adds the display panel to the CENTER position of the
// JApplet's content pane.
JPanel buttonBar = new JPanel();
// This panel will hold the button and appears
// at the bottom of the applet.
buttonBar.setBackground(Color.gray);
getContentPane().add(buttonBar, BorderLayout.SOUTH);
startStopButton = new JButton("Start");
buttonBar.add(startStopButton);
startStopButton.addActionListener( new ActionListener() {
// The action listener that responds to the
// button starts or stops the animation. It
// checks the value of timer to find out which
// to do. Timer is non-null when the animation
// is running, so if timer is null, the
// animation needs to be started.
public void actionPerformed(ActionEvent evt) {
if (timer == null)
startAnimation();
else
stopAnimation();
}
});
} // end init()
ActionListener timerListener = new ActionListener() {
// Define an action listener to respond to events
// from the timer. When an event is received, the
// color of the display is changed.
public void actionPerformed(ActionEvent evt) {
colorIndex++; // A number between 0 and 100.
if (colorIndex > 100)
colorIndex = 0;
float hue = colorIndex / 100.0F; // Between 0.0F and 1.0F.
display.setColor( Color.getHSBColor(hue,1,1) );
}
};
void startAnimation() {
// Start the animation, unless it is already running.
// We can check if it is running since the value of
// timer is non-null when the animation is running.
// (It should be impossible for this to be called
// when an animation is already running... but it
// doesn't hurt to check!)
if (timer == null) {
// Start the animation by creating a Timer that
// will fire an event every 50 milliseconds, and
// will send those events to timerListener.
timer = new Timer(50, timerListener);
timer.start(); // Make the time start running.
startStopButton.setText("Stop");
}
}
void stopAnimation() {
// Stop the animation by stopping the timer, unless the
// animation is not running.
if (timer != null) {
timer.stop(); // Stop the timer.
timer = null; // Set timer variable to null, so that we
// can tell that the animation isn't running.
startStopButton.setText("Start");
}
}
public void stop() {
// The stop() method of an applet is called by the system
// when the applet is about to be stopped, either temporarily
// or permanently. We don't want a timer running while
// the applet is stopped, so stop the animation. (It's
// harmless to call stopAnimation() if the animation is not
// running.)
stopAnimation();
}
.
.
.
This applet responds to ActionEvents from two sources:
the button and the timer that drives the animation. I decided to use
a different ActionListener object for each source. Each
listener object is defined by an anonymous nested class. (It would, of
course, be possible to use a single object, such as the applet itself,
as a listener, and to determine the source of an event by calling
evt.getSource(). However, Java programmers tend to be fond
of anonymous classes.)
The applet defines methods startAnimation() and stopAnimation(),
which are called when the user clicks the button. Each time the user clicks "Start",
a new timer is created and started:
timer = new Timer(50,timerListener);
timer.start();
Remember that without timer.start(), the timer won't do anything at all.
The constructor specifies a delay time of 50 milliseconds, so there should
be about 20 action events from the timer every second. The second parameter is
an ActionListener object. The timer events will be processed by
calling the actionPerformed() method of this object. The stopAnimation()
method calls timer.stop(), which ends the flow of events from the timer.
The startAnimation() and stopAnimation() methods also change the
text on the button, so that it reads "Stop" when the animation is running and
"Start" when it is not running.
The actionPerformed() method in timerListener responds to a
timer event by setting the color of the display. The color is computed from
a number, colorIndex that is changed for each frame. (The color is
specified as an "HSB" color. See Section 6.3
for information on the HSB color system.)
JApplet's start() and stop() Methods
There is one other method in the HelloWorldSpectrum
class that needs some explanation: the stop() method.
Every applet has several methods that are meant to be called
by the system at various times in the applet's life cycle.
We have already seen that init() is called when the
applet is first created, before it appears on the screen.
Another method, destroy() is called just before the
applet is destroyed, to give it a chance to clean things up.
Two other applet methods, start() and stop(),
are called by the system between init() and destroy()
The start() method is always called by the system just
after init(), and stop() is always called
just before destroy(). However, start()
and stop() can also be called at other times. The reason
is that an applet is not necessarily active for the whole time
that it exists.
Suppose that you are viewing a Web page that contains an applet,
and suppose you follow a link to another page. The applet still exists.
It will be there if you hit your browser's Back button to return to the
page that contains the applet. However, the page containing the applet
is not visible, so the user can't see the applet or interact with it.
The applet will not receive any events from the user, and since it is
not visible, it will not be asked to paint itself. The system calls
the applet's stop() method when user leaves the page that
contains the applet. The system will call the applet's start()
method if the user returns to that page. This lets the applet keep
track of when it is active and when it is inactive. It might want
to do this so that it can avoid using system resources when it is
inactive.
In particular, an applet should probably not leave a timer running
when it is inactive. The HelloWorldSpectrum applet defines
the stop() method to call stopAnimation(), which, in
turn, will stop the timer if it is running. When the
applet is about to become inactive, the system will call stop(),
and the animation -- if it was running -- will be stopped.
You can try it, if you are reading this page in a Web browser:
Start the animation running in the above applet,
go to a different page, and then come back to this page. You should
see that the animation has been stopped.
The animation in the HelloWorldSpectrum applet is started
and stopped under the control of the user. In many cases, we want
an animation to run for the entire time that an applet is active.
In that case, the animation can be started in the applet's start()
method and stopped in the applet's stop() method. It would also
be possible to start the animation in the init() method and
stop it in the destroy() method, but that would leave the
animation running, uselessly, while the applet is inactive. In the
following example, a message is scrolled across the page. It uses
a timer which churns out events for the whole time the applet is active:
You can find the source code in the file
ScrollingHelloWorld.java,
but the relevant part here is the start() and stop()
methods:
public void start() {
// Called when the applet is being started or restarted.
// Create a new timer, or restart the existing timer.
if (timer == null) {
// This method is being called for the first time,
// since the timer does not yet exist.
timer = new Timer(300, this); // (Applet listens for events.)
timer.start();
}
else {
timer.restart();
}
}
public void stop() {
// Called when the applet is about to be stopped.
// Stop the timer.
timer.stop();
}
These methods can be called several times during the lifetime
of an applet. The first time start() is called, it creates
and starts a timer. If start() is called again, the
timer already exists, and the existing timer is simply restarted.
Other Useful Timer Methods
Although timers are most often used to generate a sequence of
events, a timer can also be configured to generate a single event,
after a specified amount of time. In this case, the timer is
being used as an alarm which will signal the program after a
specified amount of time has passed. To use a Timer
object in this way, call setRepeats(false) after
constructing it. For example:
Timer alarm = new Timer(5000, listener);
alarm.setRepeats(false);
alarm.start();
The timer will send one action event to the listener five seconds
(5000 milliseconds) after it is started. You can cancel the alarm
before it goes off by calling its stop() method.
Here's one final way to control the timing of events:
When you start a repeating timer, the time until the first event
is the same as the time between events. Sometimes, it would be
convenient to have a longer or shorter delay before the first event.
If you start a timer in the init() method of an applet,
for example, you might want to give the applet some time to appear
on the screen before receiving any events from the timer. You can set a separate
delay for the first event by calling timer.setInitialDelay(delay),
where timer is the Timer and delay is specified
in milliseconds as usual.
Using Threads
Although Timers can replace threads in some cases, there
are times when direct use of threads is necessary. When a Timer
is used, the processing is done in an event handler. This means that
the processing must be something that can be done quickly. An event handler
should always finish its work quickly, so that the system can move on to
handle the next event. If an event handler runs for a long time, it will
block other events from being processed, and the program will become
unresponsive. So, in a GUI application, any computation or process that
will take a long time to complete should be run in a separate thread.
Then the event-handling thread can continue to run at the same time,
responding to user actions as they occur.
As a short and incomplete introduction to threads, we'll look at one
example. The example requires some explanation, but the main point
for our discussion of threads is that it is a realistic example of
a long computation that requires a separate thread.
The example is based on the Mandelbrot set, a mathematical
curiosity that has become familiar because it can be used to produce a
lot of pretty pictures. The Mandelbrot set has to do with the following
simple algorithm:
Start with a point (x,y) in the plane, where x and y are real numbers.
Let zx = x, and let zy = y.
Repeat the following:
Replace (zx,zy) with ( zx*zx - zy*zy + x, 2*zx*zy + y )
The question is, what will happen to the point (zx,zy) as the
loop is repeated over and over? The answer depends on the initial point (x,y).
For some initial points (x,y), the point (zx,zy) will, sooner or
later, move arbitrarily far away from the origin, (0,0). For other
starting points, the point (zx,zy) will stay close to (0,0)
no matter how many times you repeat the loop. The Mandelbrot set consists
of the (x,y) points for which (zx,zy) stays close to (0,0)
forever. This would probably not be very interesting, except that the Mandelbrot
set turns out to have an incredibly intricate and quite pretty structure.
To get a pretty picture from the Mandelbrot set, we change the question, just
a bit. Given a starting point (x,y), we ask, how many steps does it take,
up to some specified maximum number, before the point (zx,zy) moves some set
distance away from (0,0)? We then assign the point a color, depending on the
number of steps. If we do this for each (x,y), we get a kind of picture of
the set. For a point in the Mandelbrot set, the count always reaches the
maximum (since for such points, (zx,zy) never moves far away
from zero). For other points, in general, the closer the point is to the Mandelbrot
set, the more steps it will take.
With all that said, here is an applet that computes a picture of the Mandelbrot
set. It will begin its computation when you press the "Start" button. (Eventually,
the color of every pixel in the applet will be computed, but the applet actually
computes the colors progressively, filling the applet with smaller and smaller
blocks of color until it gets down to the single pixel level.) The applet represents
a region of the plane with -1.25 <= x <= 1.0
and -1.25 <= y <= 1.25. The Mandelbrot set
is colored purple. Points outside the set have other colors. Try it:
The algorithm for computing the colors in this applet is:
For square sizes 64, 32, 16, 8, 4, 2, and 1:
For each square in the applet:
Let (a,b) be the pixel coords of the center of the square.
Let (x,y) be the real numbers corresponding to (a,b).
Let (zx,zy) = (x,y).
Let count = 0.
Repeat until count is 80 or (zx,zy) is "big":
Let new_zx = zx*zx - zy*zy + x.
Let zy = 2*zx*zy + y.
Let zx = new_zx.
Let count = count + 1.
Let color = Color.getHSBColor( count/100.0F, 0.0F, 0.0F )
Fill the square with that color.
The point is that this is a long computation. When you click
the "Start" button of the applet, the applet creates a separate thread
to do this computation.
In Java, a thread is an object belonging to the class java.lang.Thread.
The purpose of a thread is to execute a single subroutine from beginning
to end. In the Mandelbrot
applet, that subroutine implements the above algorithm.
The subroutine for a thread is usually an instance method
public void run()
that is defined in an object that implements the interface named
Runnable. The Runnable interface defines run()
as its only method. The Runnable object is provided as
a parameter to the constructor of the thread object. (It is also possible
to define a thread by declaring a subclass of class Thread, and defining
a run() method in the subclass, but it is more common to use a Runnable
object to provide the run() method for a thread.) If runnableObject
is an object that implements the Runnable interface, then a thread
can be constructed with the command:
Thread runner = new Thread(runnableObject);
The job of this thread is to execute the run() method in runnableObject.
Just as with a Timer, it is not enough to construct a thread.
You must also start it running by calling its start method: runner.start().
When this method is called, the thread will begin executing the run() method
in the runnableObject, and it will do this in parallel with the rest of
the program. When the subroutine ends, the thread will die, and it cannot be restarted
or reused. There is no stop method for stopping a thread. (Actually, there is one, but
it is deprecated, meaning that you are not supposed to call it.) If you want to be
able to stop the thread, you need to provide some way of telling the thread to stop
itself. I often do this with an instance variable named running that
is visible both in the run() method and elsewhere in the program.
When the program wants the thread to stop, it just sets the value of running to false.
In the run() method, the thread checks the value of running regularly.
If it sees that the value of running has become false, then the
run() method should end. Here, for example, are the methods that the
Mandelbrot applet uses to start and to stop the thread:
void startRunning() {
// A simple method that starts the computational thread,
// unless it is already running. (This should be
// impossible since this method is only called when
// the user clicks the "Start" button, and that button
// is disabled when the thread is running.)
if (running)
return;
runner = new Thread(this);
// Creates a thread that will execute the run()
// method in this class, which implements Runnable.
running = true;
runner.start();
}
void stopRunning() {
// A simple method that is called to stop the computational
// thread. This is done by setting the value of the
// variable, running. The thread checks this value
// regularly and will terminate when running becomes false.
running = false;
}
There are a few more details of threads that you need to understand
before looking at the run() method from the applet. First,
on some platforms, once a thread starts running it grabs control of
the CPU, and no other thread can run until it yields
control. In Java,
a thread can yield control, and allow other threads to run, by calling
the static method Thread.yield(). I do this regularly in
the run() method of the applet. If I did not do this, then,
on some platforms, the computational thread would block the rest of the
program from running. Another way for a thread to yield control is
to go to sleep for a specified period of
time by calling Thread.sleep(time), where time is the
number of milliseconds for which the thread will be inactive. The thread
in a Timer, for example, sleeps between events, since it has
nothing else to do.
Another issue concerns the use of threads with Swing. There is a rule
in Swing: Don't touch any GUI components, or any data used by them,
except in the event-handling thread, that is, in event-handling methods
or in paintComponent(). The reason for this is that Swing
is not thread-safe. If more than one
thread plays with Swing's data structures, then the data can be
corrupted. (This is done for efficiency. Making Swing thread-safe
would slow it down significantly.) To solve this problem, Swing
has a way for another thread to get the event-handling thread to run a
subroutine for it. The subroutine must be a run() method
in a Runnable object. When a thread calls the static
method
void SwingUtilities.invokeAndWait(Runnable runnableObject)
the run() method of runnableObject will be executed in
the event-handling thread, where it can safely do anything it wants
to Swing components and their data. The invokeAndWait method,
as the name indicates, does not return until the run() method
has been executed. (The invokeAndWait method can produce an
exeception, and so must be called inside a try...catch statement.
I will cover try...catch statements in Chapter 9.
You can ignore it for now.)
The run() method for the
thread in the Mandelbrot applet uses SwingUtilities.invokeAndWait()
to color a square on the screen. Here, finally, is that run()
method (which will still take some work to understand):
public void run() {
// This is the run method that is executed by the
// computational thread. It draws the Mandelbrot
// set in a series of passes of increasing resolution.
// In each pass, it fills the applet with squares
// that are colored to represent the Mandelbrot set.
// The size of the squares is cut in half on each pass.
startButton.setEnabled(false); // Disable "Start" button
stopButton.setEnabled(true); // and enable "Stop" button
// while thread is running.
int width = getWidth(); // Current size of this canvas.
int height = getHeight();
OSI = createImage(getWidth(),getHeight());
// Create the off-screen image where the picture will
// be stored, and fill it with black to start.
OSG = OSI.getGraphics();
OSG.setColor(Color.black);
OSG.fillRect(0,0,width,height);
for (size = 64; size >= 1 && running; size = size/2) {
// Outer for loop performs one pass, filling
// the image with squares of the given size.
// The size here is given in terms of pixels.
// Note that all loops end immediately if running
// becomes false.
double dx,dy; // Size of square in real coordinates.
dx = (xmax - xmin)/width * size;
dy = (ymax - ymin)/height * size;
double x = xmin + dx/2; // x-coord of center of square.
for (i = size/2; i < width+size/2 && running; i += size) {
// First nested for loop draws one column of squares.
double y = ymax - dy/2; // y-coord of center of square
for (j = size/2; j < height+size/2 && running; j += size) {
// Innermost for loop draws one square, by
// counting iterations to determine what
// color it should be, and then invoking the
// "painter" object to actually draw the square.
colorIndex = countIterations(x,y);
try {
SwingUtilities.invokeAndWait(painter);
}
catch (Exception e) {
}
y -= dy;
}
x += dx;
Thread.yield(); // Give other threads a chance to run.
}
}
running = false; // The thread is about to end, either
// because the computation is finished
// or because running has been set to
// false elsewhere. In the former case,
// we have to set running = false here
// to indicate that the thread is no
// longer running.
startButton.setEnabled(true); // Reset states of buttons.
stopButton.setEnabled(false);
} // end run()
You can find the full source code in the file
Mandelbrot.java.
There is a lot more to be said about threads. I will not cover
it all in this book, but I will return to the topic in
Section 10.5.