Section 6.4
Threads and Animation
MUST AN APPLET BE COMPLETELY DEPENDENT on events sent from outside to get anything done? Can't an applet do something on its own initiative and on its own schedule? Something more like a traditional program that just executes a sequence of instructions from beginning to end?
The answer is yes. The applet can create a thread. A thread represents a thread of control, which independently executes a sequence of instructions from beginning to end. Several threads can exist and run at the same time. An applet -- or, indeed, any Java program -- can create several threads and start them running. Each of the threads acts like a little independent program.
One common use of threads is to do animation. A thread runs continuously while an applet is displayed. Several times a second, the thread changes the applet's display. If the changes are frequent enough, and the changes small enough, the viewer will perceive continuous motion.
Programming with threads is called parallel programming because several threads can run in parallel. Parallel programming can get tricky when several threads are all using a shared resource. An example of a shared resource would be the screen. If several threads try to draw to the screen at the same time, it's possible for the contents of the screen to become corrupted. Instance variables can also be shared resources. In order to avoid problems with shared resources, access to shared resources must be synchronized in some way. Java defines a fairly natural and easy way of doing such synchronization. I will discuss it below.
Even if an applet creates only one thread, there will still be two threads running in parallel, since there is always a user interface thread that monitors user actions and feeds events to the applet. What happens if the thread is trying to draw something at the same time that the user changes the size of the applet? This is a synchronization problem. So, even in the simple case of an apple that creates a single thread, synchronization can be an issue.
In Java, a thread is just an object of type Thread. A thread object must have a subroutine to execute. There are two ways to create a useful thread object: You can define a subclass of Thread and override the run() method in your subclass; in this case, the run() method in your subclass is the subroutine that the thread executes. Or, you can create a class that implements the interface Runnable, and pass an object of that class to a Thread constructor. The Runnable interface consists of the single method, run(). To implement this interface in a class, you must define a run() method in that class. If you construct a Thread from an object of type Runnable, then that object's run() method will be the subroutine that the thread executes.
The second method of creating threads is far more common, even though it's a little mind-bending. Typically, you might declare an applet class that also implements the Runnable interface. You could then create a thread based on the applet. You would typically do this either in the applet's init() method or in its start() method. The applet would look something like this:
public class AppletWithThread extends Applet implements Runnable { Thread runner; // thread to be executed independently . . // other stuff . public void init() { runner = new Thread(this); // create a thread based on // this applet; the thread will execute // the applet's run method . . . // other initialization } public void run() { . . . // commands to be executed by the thread } . . // other stuff . } // end of class AppletWithThreadIn this way, the run method has access to all of the applet's instance variables. All those variables, as well as the computer's screen, are potentially shared resources, since the thread can execute the run() method at the same time that the user interface thread is calling other methods from the applet.
(Applets are not special, by the way; you can implement the Runnable interface in any class you create. A runnable subclass of Canvas could be very useful, for example.)
Now, when a new Thread object is first constructed, it does not start running automatically. The thread class includes instance methods for starting and stopping the thread. (A thread will also stop automatically if it ever returns from its run() method.) Once a thread has stopped, it cannot be restarted. However, you can suspend a thread temporarily and resume it later. You can also put it to sleep for a specified period, and you can make it wait for notification of some event. If the thread object is named runner, here are some of the methods that can be called:
- runner.start(); -- causes the thread to begin execution of its associated run() method. (Note that you don't call the run() method directly.)
- runner.stop(); -- terminates the thread's execution. A thread that had been stopped by this method or that has finished execution of its run() method is said to be dead.
- runner.suspend() -- causes the thread to be suspended. The thread pauses in the middle of its execution.
- runner.resume() -- causes a thread that has been suspended to resume execution.
- runner.isAlive() -- returns a boolean value indicating whether or not the thread is still alive. Returns false if the thread is dead. (A thread that has been suspended is still considered to be alive.)
If you use a thread in an applet, you have to decide where to call these procedures. One way to organize things is to create and start the thread the first time the applet's start() method is called, and to stop it in the applet's destroy() method if it has not already stopped on its own. Since you probably don't want the thread to continue running when the applet itself is not active, you can suspend it in the applet's stop() method, and then resume it in the applet's start() method. (Alternatively you might prefer to stop the thread in the applet's stop() method and create a new thread every time the applet's start() method is called.) With this in mind, here is a more complete outline of an applet that uses a single thread:
public class AppletWithThread extends Applet implements Runnable { Thread runner = null; . . // other stuff . public void start() { if (runner == null) { // create and start the thread runner = new Thread(this); runner.start(); } else if (runner.isAlive()) { // resume the thread, runner.resume(); // unless it is already dead } } public void stop() { if (runner.isAlive()) // suspend the thread, runner.suspend(); // if it is still alive } public void destroy() { if (runner.isAlive()) // if the thread is still running, runner.stop(); // then stop it runner = null; } public void run() { . . . // subroutine to be executed by runner } } // end of class AppletWithThreadNow, you have to decide what to put in the run() method. If the thread is being used for animation, you can use an infinite loop that just keeps drawing one frame in the animation after another. You'll also want a pause between frames. Such pauses do more than space out the frames of the animation. They also give other threads a chance to run. You can insert such a pause by calling the static method Thread.sleep(int). The parameter of this method specifies the length of the pause, in milliseconds. (One thousand milliseconds equal one second.) Unfortunately, to use this method, you have to take into account the fact that it can "throw an exception," and exceptions are something that I will not cover until Chapter 8. For now, just accept that the syntax for calling this method is:
try { Thread.sleep(sleepTime); } catch (InterruptedException e) { }where sleepTime is the number of milliseconds that you would like the thread to sleep.
Suppose, for example, that you want your applet to scroll the message "Hello World" from right to left across the applet. You might use instance variables, x and y, to control the position of the message and rely on the paint() method to draw the message to the screen. The run method would animate the message by changing the value of x for each frame, and by calling repaint() to see that each frame is drawn. So, the applet would contain the following:
int x; // x and y coordinates of starting point of string int y = -1; // This value indicates that the value of y // has not yet been set by the run() method String message = "Hello World"; int sleepTime = 100; // milliseconds between frames public void paint(Graphics g) { if (y > 0) g.drawString(message,x,y); } public void run() { FontMetrics fm = getFontMetrics(getFont()); // get a FontMetrics object for the applet's font int stringWidth = fm.stringWidth(message); // use fm to determine the size of the message // when it is displayed int min_x = -stringWidth; // smallest acceptable value for x, // when the message has scrolled // entirely off the left edge of // the applet. int max_x = size().width; // largest acceptable value for x, // when the message is just // outside the right edge. int dx = 5; // number of pixels that the message moves // between one frame to the next x = max_x; // set starting position for message y = fm.getHeight(); while (true) { // loop forever x = x - dx; // move the message if (x < min_x) // if message is at left end of its path, x = max_x; // then move it back to the beginning repaint(); // ask system to redraw the applet try { Thread.sleep(sleepTime); } // pause before next frame catch (InterruptedException e) { } } } // end of run()Unfortunately, while this will indeed scroll the message across the applet, it will cause the applet to flicker terribly as the message is continually erased and repainted. A better solution is to use an off-screen image. The run() method can draw each frame to the off-screen image, and all the paint() method has to do is copy the image to the screen. However, this brings a new problem: The off-screen image is a shared resource. What happens if paint() is copying the image to the screen while the run() method is in the middle of drawing the next frame? The screen will become corrupted. Somehow, access to the off-screen image must be controlled so that only one thread can use it at a time.
In Java, access to shared resources can be controlled by using synchronized methods. An instance method is declared to be a synchronized method by adding the modifier synchronized to its declaration. For example:
synchronized void drawOneFrame() { ... }
You can even add this modifier to standard methods, such as paint. The meaning is that only one synchronized method can be active in an object at any given time. If a thread tries to call a synchronized method, and if that method or some other synchronized method in the same object is already being executed by a different thread, then the thread will have to wait until that other thread has finished executing its method. This is all handled automatically by the system. All you have to do is declare the methods to be synchronized.
If all access to shared resources is done in synchronized methods, then you can be sure that at any given time, only one thread will ever have access to those resources. You can be sure that one thread will have a chance to finish with a resource before another thread will have a chance to use it.
To apply synchronization to animation with an off-screen image, you can define a synchronized paint method that copies the off-screen image to the screen:
Image OSC = null; // the off-screen image synchronized public void paint(Graphics g) { if (OSC != null) // if off-screen image exists, copy it to screen g.drawImage(OSC,0,0,this); else { // otherwise, fill the applet with the background color g.setColor(getBackground()); g.fillRect(0,0,size().width,size().height); } } public void update(Graphics g) { // redefine update() so that it doesn't erase the applet; // this will avoid flickering when the applet is redrawn paint(g); }The off-screen image must be created at some point before any frames can be drawn on it. One place to do this is in the start() method of the applet, just before the thread is created:
public void start() { if (runner == null) { // create off-screen image and thread OSC = createImage(size().width, size().height); runner = new Thread(this); runner.start(); } else if (runner.isAlive()) { // resume the thread, runner.resume(); // unless it is already dead } }In addition to this, you should create a synchronized method for drawing the frames of the animation to the off-screen image. That method should be called by the run() method to create the frames:
synchronized void drawNextFrame() { . . . // draw the next frame of the animation // Note: this method needs either parameters or access // to instance variables that describe the frame. } public void run() { . . . // do some initialization while (true) { . . . // do any updating required for next frame // (This could also be done in drawNextFrame().) drawNextFrame(); repaint(); try { Thread.sleep(sleepTime); } catch (InterruptedException e) { } } }Using this method, you could write an applet that scrolls a message smoothly across the screen without any flickering. You might even do something a bit fancier.
[ Next Chapter | Previous Section | Chapter Index | Main Index ]