New to Java? We'll help you get started with our revised beginner's tutorial, or our free online textbook.
|
Get the latest Java books |
|
h t t p : / /w w w . j a v a c o f f e e b r e a k . c
o m /
|
Section 7.5
Threads, Synchronization, 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 one or more threads and start them running. Each of the threads acts as a little program. The separate threads run independently but they can communicate with each other by sharing variables.
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 is 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.
Note that on most computers, you can't literally have two threads running "at the same time," since there is only one processor, which can only do one thing at a time. Only computers with more than one processor can literally do more than one thing at a time. However, this does not solve the synchronization problem on single-processor computers! A single-processor computer simulates multiprocessing by switching rapidly from thread to thread. Without proper synchronization, a thread can be interrupted at any time to let another thread run, and this can cause problems. Suppose, for example that a thread reads the value of an important variable just before it is interrupted. After the thread resumes, it makes a decision based on the value it just read. The problem is that, without proper synchronization, some other thread might have butted in and changed the value of the variable in the meantime! The decision that the thread makes is based on a value that is no longer necessarily valid. Synchronization can be used to make sure this doesn't happen.
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? What if one thread is drawing to an off-screen image at the same time another thread is copying that image to the screen? These are synchronization problems. 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. The class Thread is defined in the standard package java.lang. A thread object must have a subroutine to execute. That subroutine is always named run(), but there are two different places where the run() method might be located, depending on how the thread is programmed.
The Thread class itself defines a run() method, which doesn't do anything. One way to make a useful thread is to define a subclass of Thread and override the run() method in your subclass to make it do something useful.
A second way to program a thread -- and the only one I will use for the time being -- is to create a class that implements the interface called Runnable. This interface defines one method, "public void run()". (To implement this interface means to declare that the class "implements Runnable" and to define the method "public void run()" in the class.) A thread can be constructed from an object of type Runnable. When such a thread is run, it executes the run() method from the Runnable object. I'll give an example of all this in just a moment. The advantage of defining a thread in this way is that the run() method has access to all the instance variables of the object, so that the thread will be able to read and change the values of those variables. (A disadvantage is that throwing a run() method into a class can completely muddle the clear division of responsibilities that should be the hallmark of object-oriented programming. The run() method is logically part of the Thread object, but it is physically part of the Runnable object. Keep this in mind: It's best to think of the run() method as a separate entity.)
Suppose that runnableObject is an object that implements Runnable. And suppose that runner is a variable of type Thread. Then the statement
runner = new Thread(runnableObject);
creates a thread that can execute the run() method of runnableObject. However, the thread does not automatically start running. To get it to run, you have to call its start() method:
runner.start();
The thread will then begin executing the run() method. At the same time, the rest of your program continues to execute. The thread continues until it reaches the end of the run() method. At that point it "dies" and cannot be restarted or reused. If you want to execute the same run() method again, you have to create a new thread to do it. You can check whether a thread is still alive by calling its isAlive() method, which returns a boolean value:
if (runner.isAlive()) . . .
(Note: it is possible to kill a thread before it finishes normally by calling its stop() method. However, use of this method is discouraged since it is error-prone, and I will avoid it entirely.)
As we work through a few examples in the rest of this section, I'll be introducing several important new ideas. One of the ideas has to do with managing the state of an applet. The state of an applet consists of its instance variables. The state determines how the applet will respond when its methods are called. Managing the state means determining how and when the state should change. In many cases, it also means making the state of the applet apparent to the user, so the user isn't surprised or frustrated by the applet's behavior. One way to make the state of the applet apparent is to disable a button whenever clicking on that button would make no sense. Recall that a button, bttn, can be disabled with the command "bttn.setEnabled(false);", and it can be enabled with "bttn.setEnabled(true);".
Let's look at the first example. When you click on the button in the following applet, a thread is created that blinks the color of the message from red to green and back again several times. Note that the button is disabled while the message is blinking:
The source code for this applet begins with the lines:
public class BlinkingHelloWorld1 extends Applet implements ActionListener, Runnable { ColoredHelloWorldCanvas canvas; // Canvas that displays the message Button blinkBttn; // The "Blink at Me" button. Thread blinker; // A thread that cycles the message colors.In this example, the applet class itself implements Runnable. (There is nothing special about applets in this regard. Any class can implement Runnable. It might be useful, for example, to have a Runnable canvas.) Later in the source code, the thread will be created with the command "blinker = new Thread(this);", where this refers to the applet object itself. This means that the thread we create will execute whatever run() method is defined in the BlinkingHelloWorld1 class. Let's think about what we want this thread to do. (This is just like designing a small program.) We want the thread to blink the color of the message. We'll also give the thread the responsibility of disabling and enabling the button, since that way we can be sure that the button will be disabled just as long as the message is blinking. A pseudocode version of the run method would be:
disable the blinkBttn set the text color to green pause for a while set the text color to red pause for a while set the text color to green pause for a while set the text color to red pause for a while set the text color to green pause for a while set the text color to red enable the blinkBttnThe pauses are necessary since otherwise the blinking would go by much too fast to see. Unfortunately, for technical reasons, getting a thread to pause requires a try...catch statement, which will not be covered until Chapter 9. For the time being, I will just give you a subroutine that can be called by a thread to insert a pause in its execution:
void delay(int milliseconds) { // Pause for the specified number of milliseconds, // where 1000 milliseconds equal one second. try { Thread.sleep(milliseconds); } catch (InterruptedException e) { } }Using this subroutine and a setTextColor() routine from the ColoredHelloWorldCanvas class, the applet's run() method becomes:
public void run() { blinkBttn.setEnabled(false); canvas.setTextColor(Color.green); delay(300); canvas.setTextColor(Color.red); delay(300); canvas.setTextColor(Color.green); delay(300); canvas.setTextColor(Color.red); delay(300); canvas.setTextColor(Color.green); delay(300); canvas.setTextColor(Color.red); blinkBttn.setEnabled(true); }The blinker thread, which executes this run method, has to be created and started when the user clicks the "Blink at Me!" button. This is done in the applet's actionPerformed() method, which is called when the user clicks the button:
public void actionPerformed(ActionEvent evt) { if ( blinker == null || (blinker.isAlive() == false) ) { blinker = new Thread(this); blinker.start(); } }This routine creates and starts a thread. That thread executes the above run() method, which blinks the text several times. When the run method ends, the thread dies.
I only want one of these threads to be running at a time. To be sure of this, before creating the new thread, I test whether another thread already exists and is running. Since the button is disabled while a thread is running, this test might seem unnecessary. However, it's usually better to test a condition rather than just assume it's true. And in this case, it's just possible that the user might manage to click the button a second time before the first thread gets started.
The complete source code for this example can be found in the file BlinkingHelloWorld1.java.
In the previous example, the thread that was created ran for only a short time, until it had run through all the instructions in its run() method. In many cases, though, we want a thread to run over an extended period. Often a thread that is created by an applet should run as long as the applet itself exists. Before working with such threads, though, you need to know more about an applet's life cycle.
An applet, just like any other object, has a "life cycle." It is created, it exists for a time, and it is destroyed. The Applet class defines an init() method, which is called just after the applet is created. There are other methods in the Applet class which are called at other points in an applet's life cycle. A programmer can provide definitions of these methods if there are tasks that the programmer would like to have executed at those points in the applet's life cycle.
Just before an applet object is destroyed, the method "public void destroy()" is called to give the applet a chance to clean up before it ceases to exist. Because of Java's automatic garbage collection, a lot of cleanup is done automatically. There are a few cases, however, where destroy() might be useful. In particular, if the applet has created any threads, those threads should almost certainly be stopped before the applet is destroyed. The destroy() method is a natural place to do this. As another example, it is possible for an applet to create a separate window on the screen. The applet's destroy method could close such a window so it doesn't hang around after the applet no longer exists.
An applet also has methods "public void start()" and "public void stop()". These play similar roles to init() and destroy(). However, while init() and destroy() are each called exactly once during the life cycle of an applet, start() and stop() can be called many times. The start() method is definitely called by the system at least once when the applet object is first created, just after the init() method is called. The stop() method will definitely be called at least once, before the applet is destroyed. In addition to these calls, the system can choose to stop the applet by calling its stop() method at any time and then later restart it by calling its start() method again. For example, a Web browser will typically stop an applet if the user leaves the page on which the applet is displayed, and will restart it if the user returns to that page. An applet that has been stopped will not receive any other events until it has been restarted.
It is not always clear what initialization should be done in init() and what should be done in start(). Things that only need to be done once should ordinarily be done in init(). If the applet creates a separate thread to carry out some task, it is reasonable to start that thread in the start() method and stop it in the stop() method. If the applet uses a large amount of some computer resource such as memory, it might be reasonable for it to allocate that resource in its start() method and release it in its stop() method, so that the applet will not be hogging resources when it isn't even running. On the other hand, sometimes this is impossible because the applet needs to retain the resource for its entire lifetime.
The next example creates a thread that runs until the user clicks on a button, or until the applet itself is stopped:
When you click on the "Blink!" button, the message starts cycling between two colors. The name of the "Blink!" button changes to "Stop!". Clicking on the "Stop!" button will stop the blinking. (The command that changes the name of the button, blinkBttn, to "Stop!" is blinkBttn.setLabel("Stop!"). This is another case of changing the appearance of the applet when it changes state in order to help the user understand what is going on.) The other two buttons can be used at any time to set the colors that are used for the blinking message. Recall that the blinking is handled by one thread while the button events are handled by a separate user interface thread, so in this example you really do get to see two threads operating in parallel.
The run() method for this example has a while loop that makes it blink the text over and over. The interesting question is how to get the loop to end and the thread to stop running when the user clicks the "Stop!" button. The answer is to use a shared instance variable for communication between the thread and the applet. The thread tests the value of the variable and keeps running as long as the variable has a certain value. When the user clicks on the "Stop!" button, the applet changes the value of the variable. The thread sees this change and responds by exiting from its run() method and dieing.
I tend to use a variable named status for this type of communication. It's a good idea, for the sake of readability, to use named constants as the possible values of status. For this example, the status information that I need is whether the thread should continue or should end. I use constants named GO and TERMINATE to represent these two possibilities. The thread continues running as long as the value of status is GO. It ends when the value of status changes to TERMINATE. To implement this, the applet includes the following instance variables:
private final static int GO = 0, // For use as values of status. TERMINATE = 1; private volatile int status; // This is used for communication between // the applet and the thread. The value is // set by the applet to tell the thread what // to do. When the applet wants the thread // to terminate, it sets the value of status // to TERMINATE.(The word volatile is a new modifier that has to do with communication between threads. It should be used on a variable whose value is set by one thread and read by another. The somewhat technical reason is this: If a variable is not declared to be volatile, then on some computers, when one thread changes the value of the variable, other threads might not immediately see the new value. This is another type of synchronization problem. Later in this section, I'll mention "synchronized" methods and statements. Variables that are accessed only in synchronized methods and statements don't have to be declared volatile.)
The idea is for the thread to run just so long as the value of status is GO. The thread's run() method tests the value of status in a while loop, which continues only so long as status == GO. Note that the value of status must be set equal to GO before the thread is started, since otherwise the run() method would finish immediately. When the user clicks on the "Blink!" button, the following commands are executed to start the thread:
runner = new Thread(this); status = GO; runner.start();In addition to blinking the text, the thread is also responsible for changing the label on the button. Here is the entire run() method for the thread:
public void run() { blinkBttn.setLabel("Stop!"); while (status == GO) { waitDelay(300); changeColor(); } blinkBttn.setLabel("Blink!"); }Here, the waitDelay() method imposes a delay of 300 milliseconds, while changeColor() changes the color of the displayed message. Both these methods are defined elsewhere in the applet.
The thread is started and stopped in the actionPerformed() method, in response to the user clicking on the Blink/Stop button. When the user clicks "Stop!", the value of status is set to TERMINATE. The thread sees this value and stops. But we have to be careful. What if the user never clicks on "Stop!"? We should be careful not to leave the thread running after the applet is destroyed. An applet that creates threads that might otherwise run forever should make sure to terminate them, either in its stop() method or in its destroy() method. In this example, I use the stop() method to stop the thread by setting status to TERMINATE. The stop() method will be called by the system if you close the window that displays this page or follow a link to another page (or, in Netscape, if you just resize the page). So, if you start the message blinking, go to another page, and then return to this one, the blinking should be stopped.
In the second blinking hello world applet, there are two reasons why the color of the message might change: because the runner thread changes it or because the user clicks on the "Red/Green" button or the "Black/Blue" button. These two reasons are handled by two different threads. The variables that record the current color are resources that are shared by two threads. When a thread is accessing a shared resource, it usually needs exclusive access to that resource, so that no other thread can butt in and access the resource at the same time. In Java, exclusive access is implemented using synchronized methods and synchronized statements. A method is declared to be synchronized by adding the synchronized modifier to its definition. Here, for example, is the method that is called by the runner thread to change the color of the message:
synchronized void changeColor() { // Change from first to second color or vice versa. if (showingFirstColor) { // Change to showing second color. showingFirstColor = false; if (useRedAndGreen) canvas.setTextColor(Color.green); else canvas.setTextColor(Color.blue); } else { // Change to showing first color. showingFirstColor = true; if (useRedAndGreen) canvas.setTextColor(Color.red); else canvas.setTextColor(Color.black); } } // end changeColor()The other method that can change the color of the text is the actionPerformed() method, which is called by the user interface thread when the user clicks on one of the buttons. Like the changeColor() method, the actionPerformed() method is declared to be synchronized. This means that all the code that does color changes is contained inside synchronized methods. Therefore, only one of the color-change processes can be running at any given time, and there is no possibility of their interfering with each other.
Here, briefly, is how such synchronization is implemented in Java: Every object in Java has an associated lock. The lock can be "held" by a thread, but only one thread can hold the lock at a given time. The rule is that in order to execute a synchronized method in an object, a thread must obtain that object's lock. If the lock is already held by another thread, then the second thread must wait until the first thread releases the lock. If all access to a shared resource takes place inside methods that are synchronized on the same object, then each thread that accesses the resource has exclusive access.
When a resource is only used in part of a method, it's not necessary to make the entire method synchronized. A single statement can be synchronized. The synchronized statement takes the form
synchronized( object ) { statements }(The synchronized statement, for some unknown reason, requires the braces { and } even if they contain just a single statement.) To execute the statements, a thread must first obtain the lock belonging to the specified object. Often, you will say "synchronized(this)" to synchronize on the same object that contains the method. However, it is also possible to synchronize on another object's lock.
What could go wrong in the sample applet if the methods were not synchronized? For example: When the blinker thread executes, the statement
if (useRedAndGreen) canvas.setTextColor(Color.green); else canvas.setTextColor(Color.blue);it is possible that just after the thread tests the value of useRedAndGreen, the other thread butts in and changes the value of useRedAndGreen. Then the blinker thread resumes and changes the text color based on the value of useRedAndGreen that it saw. However, that value is no longer valid, and the thread changes the text to an incorrect color. The state of the applet has become inconsistent. The variables say the color should be one thing, but the actual color is different. It's not a big deal here, but there are cases where something like this -- even if it happens very, very rarely -- could be a really big deal.
The complete source code for this example is in the file BlinkingHelloWorld2.java.
In the rest of this section, I will try to explain the most advanced aspect of synchronization, the wait() and notify() methods. These methods are defined in the Object class, and so they can be used with any object. In order to legally call an object's wait() method, a thread must hold that object's lock. So wait() is usually called only in synchronized methods and statements.
A thread calls an object's wait() method when it wants to wait for some event to occur. (While it is waiting, it releases its hold on the lock, so that other threads can run.) Some other thread must call the same object's notify() method when the event occurs. When an object's notify() method is called, any threads that are waiting on that object will wake up and can continue. Obviously, this requires close coordination between several threads, which means very careful programming.
If notify() is never called, it's possible that a waiting thread might wait forever. You can avoid this with good programming, but you can also put a time limit on how long the thread will wait. This is done by passing a parameter to the wait() method specifying the maximum number of milliseconds that the thread will wait. If notify() has not been called by the end of that time period, the thread will wake up anyway.
As with an earlier example on this page, use of the wait() method requires a try...catch statement. Here are two methods that can be called to wait indefinitely or for a specified time period:
synchronized void waitDelay(int milliseconds) { // Pause for the specified number of milliseconds OR // until the notify() method is called by some other thread. try { wait(milliseconds); } catch (InterruptedException e) { } } synchronized void waitDelay() { // Pause until the notify() method is called // by some other thread. try { wait(); } catch (InterruptedException e) { } }The first of these can be used as a kind of interruptable delay. A thread that calls it will pause for the specified number of milliseconds, unless a call to notify() occurs in the meantime. I use this waitDelay() method in the run() method of the BlinkingHelloWorld2 applet. Whenever I set the status variable in that applet to TERMINATE, I call notify(). If the thread is in the middle of a waitDelay(), this will wake it up. Suppose you click on the "Stop!" button just after the runner thread calls waitDelay(300). Because of the call to notify(), the runner thread wakes up and terminates immediately, instead of continuing to sleep for 300 milliseconds before terminating. This avoids a noticeable delay between the time you click on the button and the time that its name changes back to "Blink!". However, this is still a pretty trivial use of wait() and notify(). The final example in this section shows how to use them in a non-trivial way. It also provides an example of using an off-screen canvas to do smooth animation. Here's the last "Hello World" you'll see in these notes:
The complete source code for this applet can be found in the file ScrollingHelloWorld.java. I'll only look at a few aspects of it here.
The thread that animates this applet runs continually as long as this page is visible. The thread is created when the applet is first started, and it is stopped when the applet is destroyed. However, during periods when the applet is stopped, the thread is not actively running. It is just waiting to be notified when the applet is restarted. This behavior is programmed using the applet's wait() and notify() methods.
The runner thread in this applet has three possible statuses: GO to tell it to run the animation; TERMINATE to tell it to terminate because the applet is about to be destroyed; and SUSPEND to tell it that the applet has been stopped, and that it should go to sleep and wait for the applet to be restarted. The applet's start() method sets the status to GO. If the thread does not yet exist, it is created and started. Otherwise, the existing thread is notified that the value of status has changed. This will wake up a thread that has been put to sleep by a previous call to stop():
synchronized public void start() { // Called when the applet is being started or restarted. // Create a new thread or restart the existing thread. status = GO; if (runner == null || ! runner.isAlive()) { // Thread doesn't yet exist or has died for some reason. runner = new Thread(this); runner.start(); } else { // Another thread exists and is still alive, but notify(); // is presumably sleeping. Wake it up. } }The stop() and destroy methods merely change the value of status and notify the thread that the value has changed:
synchronized public void stop() { // Called when the applet is about to be stopped. // Suspend the thread. status = SUSPEND; notify(); } synchronized public void destroy() { // Called when the applet is about to be permanently // destroyed. Stop the thread. status = TERMINATE; notify(); }The run() method for this example uses the following while loop to run the animation:
while (status != TERMINATE) { synchronized(this) { while (status == SUSPEND) waitDelay(); } if (status == GO) nextFrame(); if (status == GO) waitDelay(250); }This loop is repeated until status becomes equal to TERMINATE. The synchronized statement in this loop,
synchronized(this) { while (status == SUSPEND) waitDelay(); }causes the thread to pause as long as the status is SUSPEND. It's good practice here to use a while statement rather than an if statement, since in general notify() could be called for several different reasons. Just because notify() has been called, it doesn't necessarily mean that the value of status has changed from SUSPEND to something else.
You might wonder why this is synchronized. If it were not synchronized, the following sequence of events would be possible: (1) The runner thread finds that the value of status is SUSPEND and decides to call waitDelay(); (2) some other thread butts in, changes the value of status and calls notify(); (3) the runner thread resumes and calls waitDelay() after notify() has already been called. Then the waitDelay() will cause the thread to wait for a notify() that has already occurred and is not going to occur again. This would be a minor disaster: The animation will not properly restart, or the thread will not properly terminate. Again, in other circumstances, the disaster could be major.
[ Next Section | Previous Section | Chapter Index | Main Index ]