Solution for
Programming Exercise 7.6
THIS PAGE DISCUSSES ONE POSSIBLE SOLUTION to the following exercise from this on-line Java textbook.
Exercise 7.6: The StopWatch component from Section 7.4 displays the text "Timing..." when the stop watch is running. It would be nice if it displayed the elapsed time since the stop watch was started. For that, you need to create a Thread. Add a Thread to the original source code, StopWatch.java, to display the elapsed time in seconds. Create the thread in the mousePressed() routine when the timer is started. Stop the thread in the mousePressed() routine when the timer is stopped. The elapsed time won't be very accurate anyway, so just show the integral number of seconds. You only need to set the text a few times per second. In my run() method, I insert a delay of 100 milliseconds after I set the text. Here is an applet that tests my solution to this exercise:
Discussion
The applet follows guidelines for using threads from Section 7.5. There is a variable called runner that holds a reference to the Thread object. I create a new thread each time the timer is started. (An alternative would be to have a single thread. The thread would be awakened when the timer is started and would be suspended when the timer is stopped. There is no particular reason for preferring one of these alternatives to the other.) There is a status variable whose possible values are given by constants named GO and TERMINATE. The applet sets the value of status to GO before it creates the thread. The thread runs as long as the value of status is not TERMINATE. When it's time for the thread to die, the applet sets the value of status to TERMINATE. Since the status variable is shared, all access to the variable takes place in synchronized methods and statements. All this is standard procedure for working with threads. (My standard procedure, that is. There is no general standard that I know of.)
The run() method that is executed by the thread just sets the text on the label about 10 times a second. (The text actually changes just once per second. Setting it 10 times per second ensures that the displayed value is reasonably accurate.) The elapsed time is computed by subtracting the time when the thread started from the current time. The 100 millisecond delay is accomplished by calling waitDelay(), a method that was given in Section 7.5. The run() method will end when the status changes to TERMINATE, and the thread will die.
public void run() { // Run method executes while the timer is going. // Several times a second, it computes the number of // seconds the thread has been running and displays the // whole number of seconds on the label. It ends // when status is set to TERMINATE. long start; // Time when thread starts. start = System.currentTimeMillis(); while (true) { synchronized(this) { if (status == TERMINATE) break; long time = (System.currentTimeMillis() - start) / 1000; setText( "Running: " + time + " seconds"); } waitDelay(100); } }(Programming Note: Originally, I didn't declare the variable "start" in the run method. Instead, I just used an instance variable, startTime, as the start time. This variable is set to the time when the user clicked the mouse, evt.getWhen(), in the mousePressed() method. This worked fine on my computer. But when I looked at the applet on another computer, the time was wrong! Apparently, the times used by the System.currentTimeMillis() and by evt.getWhen() were different. They used different times for time zero. According the Java documentation I have, this is incorrect behavoir. However, it's typical of Java programming that you have to code around problems on specific platforms. Unfortunately, although Java is supposed to be "write once, run anywhere", it doesn't quite work out that way.)
The thread is started and stopped in the mousePressed() method. This method needs a way to tell whether the mouse click that it is processing is starting the timer or stopping it. In the original version, a boolean-valued variable named running was used to keep track of this. In this version, the thread variable, runner, is non-null when the timer is running and is null when the timer is not running. So I got rid of the old "running" variable and test the value of runner instead.
synchronized public void mousePressed(MouseEvent evt) { // React when user presses the mouse by // starting or stopping the timer. if (runner == null) { // Since runner is null, the timer is not running. // Record the time and start the timer by creating a thread. startTime = evt.getWhen(); // Time when mouse was clicked. status = GO; runner = new Thread(this); runner.start(); } else { // Stop the thread. Compute the elapsed time since the // timer was started and display it. status = TERMINATE; notify(); // Wake up thread so it can terminate quickly. long endTime = evt.getWhen(); double seconds = (endTime - startTime) / 1000.0; setText("Time: " + seconds + " seconds"); runner = null; } }If runner is null, the mousePressed method records the start time of the timer and it creates a new thread. The thread will almost immediately set the display of the label to "Running: 0 seconds". When the user clicks again, the value of runner is not null. The mousePressed() method stops the thread and it sets the text on the label to show the exact time that the timer was running. (It would have been possible to let the thread change the display as its last act. In that case, the endTime variable would have to be an instance variable so that the thread would have access to it.)
Note that the mousePressed() method is synchronized. This is important since it accesses the shared variable, status. If it were not synchronized, the following sequence of events would be possible: (1) The thread tests status and finds that the value is GO. At that moment, the tread is interrupted. (2) The mousePressed() method changes the value of status to TERMINATE and sets the display to "Time...". (3) The thread resumes and goes on to change the display to "Running..." even though the timer has been stopped. Only then does it test the value of status again and see that the value has changed to TERMINATE. The label continues to show "Running..." instead of the final time. (Remember that "synchronized" does not mean "cannot be interrupted". It means, essentially, "cannot be interrupted by someone else who is synchronized on the same object". Both parties have to be synchronized.)
The complete source code is shown below, followed by the source code for the little applet that tests the component.
The Solution
The improved StopWatch component:
/* A component that acts as a simple stop-watch. When the user clicks on it, this component starts timing. When the user clicks again, it displays the time between the two clicks. Clicking a third time starts another timer, etc. While it is timing, the label just displays the whole number of seconds since the timer was started. */ import java.awt.*; import java.awt.event.*; public class StopWatchRunner extends Label implements MouseListener, Runnable { private Thread runner; // A thread that runs as long as the timer // is running. It sets the text in the label // to show the elapsed time. This variable is // non-null when the timer is running and is // null when it is not running. (This is // tested in mousePressed().) private static final int // Constants for use with status variable. GO = 0, TERMINATE = 1; private int status; // This variable is set by mousePressed() // to control the thread. When it's time // for the thread to end, the value is // set to TERMINATE. private long startTime; // Start time of timer. // (Time is measured in milliseconds.) public StopWatchRunner() { // Constructor. Call the constructor from the superclass // and listen for mouse clicks. super(" Click to start timer. ", Label.CENTER); addMouseListener(this); } synchronized public void destroy() { // Before an applet that uses this component is // destroyed, it can call this method to make sure // that, if a timer thread is still running, it // will stop. (This method is NOT called by the system!) status = TERMINATE; } public void run() { // Run method executes while the timer is going. // Several times a second, it computes the number of // seconds the thread has been running and displays the // whole number of seconds on the label. It ends // when status is set to TERMINATE. long start; // Time when thread starts. start = System.currentTimeMillis(); while (true) { synchronized(this) { if (status == TERMINATE) break; long time = (System.currentTimeMillis() - start) / 1000; setText( "Running: " + time + " seconds"); } waitDelay(100); } } synchronized void waitDelay(int milliseconds) { // Pause for the specified number of milliseconds OR // until the notify() method is called by some other thread. // (From Section 7.5 of the text.) try { wait(milliseconds); } catch (InterruptedException e) { } } synchronized public void mousePressed(MouseEvent evt) { // React when user presses the mouse by // starting or stopping the timer. if (runner == null) { // Since runner is null, the timer is not running. // Record the time and start the timer by creating a thread. startTime = evt.getWhen(); // Time when mouse was clicked. status = GO; runner = new Thread(this); runner.start(); } else { // Stop the thread. Compute the elapsed time since the // timer was started and display it. status = TERMINATE; notify(); // Wake up thread so it can terminate quickly. long endTime = evt.getWhen(); double seconds = (endTime - startTime) / 1000.0; setText("Time: " + seconds + " seconds"); runner = null; } } public void mouseReleased(MouseEvent evt) { } public void mouseClicked(MouseEvent evt) { } public void mouseEntered(MouseEvent evt) { } public void mouseExited(MouseEvent evt) { } } // end StopWatchA small applet to test the component:
/* A trivial applet that tests the StopWatchTimer component. The applet just creates and shows a StopWatchTimer. */ import java.awt.*; import java.applet.*; public class TestStopWatchRunner extends Applet { public void init() { StopWatchRunner watch = new StopWatchRunner(); watch.setFont( new Font("SansSerif", Font.BOLD, 24) ); watch.setBackground(Color.white); watch.setForeground( new Color(180,0,0) ); setBackground(Color.white); setLayout(new BorderLayout() ); add(watch, BorderLayout.CENTER); } }
[ Exercises | Chapter Index | Main Index ]