Chapter 4. Implementing Runnable Versus Extending Thread

IN THIS CHAPTER

Until now, extending the class Thread has been the only way to define a new class that can have a thread running within it. This chapter shows that the Runnable interface provides a second, and more often used, mechanism for defining a new class that can have a thread running within it.

Visual Timer Graphical Component

Imagine that what you need is a timer graphical component that continually displays the time elapsed since it was started. To build this custom component, at a bare minimum, you must extend Component. Because this example uses Swing, you will instead extend JComponent (which indirectly extends Component). Figure 4.1 shows the initial class hierarchy for the new customized component, SecondCounter.

The initial class hierarchy for SecondCounter.

Figure 4.1. The initial class hierarchy for SecondCounter.

SecondCounter IS-A Component, so it can be added to any Container, just like the other Components. This SecondCounter has to keep track of the amount of time that has passed since it was started and update itself every 0.1 seconds to visually reflect the time that has elapsed.

Listing 4.1 shows the source code for a first cut at defining this class. This version definitely has serious problems, but it illustrates the necessity of another approach. To keep the evolving versions of SecondCounter straight, slightly different classnames are used for each version. In this case, the class name is SecondCounterLockup.

Example 4.1. SecondCounterLockup.java—The First Attempt at the Timer

 1: import java.awt.*;
 2: import javax.swing.*;
 3: import java.text.*;
 4:
 5: public class SecondCounterLockup extends JComponent {
 6:     private boolean keepRunning;
 7:     private Font paintFont;
 8:     private String timeMsg;
 9:     private int arcLen;
10:
11:     public SecondCounterLockup() {
12:         paintFont = new Font("SansSerif", Font.BOLD, 14);
13:         timeMsg = "never started";
14:         arcLen = 0;
15:     }
16:
17:     public void runClock() {
18:         System.out.println("thread running runClock() is " +
19:                 Thread.currentThread().getName());
20:
21:         DecimalFormat fmt = new DecimalFormat("0.000");
22:         long normalSleepTime = 100;
23:
24:         int counter = 0;
25:         keepRunning = true;
26:
27:         while ( keepRunning ) {
28:             try {
29:                 Thread.sleep(normalSleepTime);
30:             } catch ( InterruptedException x ) {
31:                 // ignore
32:             }
33: 
34:             counter++;
35:             double counterSecs = counter / 10.0;
36:
37:             timeMsg = fmt.format(counterSecs);
38:
39:             arcLen = ( ( ( int ) counterSecs ) % 60 ) * 360 / 60;
40:             repaint();
41:         }
42:     }
43:
44:     public void stopClock() {
45:         keepRunning = false;
46:     }
47:
48:     public void paint(Graphics g) {
49:         System.out.println("thread that invoked paint() is " +
50:                 Thread.currentThread().getName());
51:
52:         g.setColor(Color.black);
53:         g.setFont(paintFont);
54:         g.drawString(timeMsg, 0, 15);
55:
56:         g.fillOval(0, 20, 100, 100);  // black border
57:
58:         g.setColor(Color.white);
59:         g.fillOval(3, 23, 94, 94);  // white for unused portion
60:
61:         g.setColor(Color.blue);  // blue for used portion
62:         g.fillArc(2, 22, 96, 96, 90, -arcLen);
63:     }
64: }

This component draws itself with a text message and a circular dial. Initially, the text message is "never started" (line 13), and the dial is totally white (arcLen = 0, line 14). After the timer is started, the text message indicates the total elapsed time in fractional seconds. The dial sweeps out a blue-filled arc in a clockwise direction that completes 360 degrees every 60 seconds. The dial portion is very similar to the second hand on an analog watch or clock.

The paint() method (lines 48–63) handles the drawing of the component based on the current values of timeMsg and arcLen. In addition, on lines 49 and 50, paint() reveals the name of the thread that invoked it.

The runClock() method is called when the timer should begin counting (line 17), and it also shares the name of the thread that invoked it (lines 18 and 19). On line 21, the format for the textual display of the seconds elapsed is defined to show fractional seconds down to the millisecond (ms). The normalSleepTime is defined as 100ms, which is the 0.1-second interval between updates that was desired. The number of iterations is held in counter (line 24). Initially, keepRunning is set to true to indicate that the timer should continue to run (line 25). The remainder of the method is a while loop (lines 27–41). In this loop, a quick nap is taken for 1/10 second (lines 28–32). Then, counter is incremented to indicate that another 0.1 seconds has passed (line 34). This count is converted to seconds: counterSecs (line 35). On line 37, the number of seconds is formatted into a String for display in the paint() method. The arc length in degrees is calculated for use in the paint() method (line 39). Finally, repaint() is called to let the JavaVM know that it should schedule a call to paint() as soon as it can.

The method stopClock() (lines 44–46) is invoked to signal that the timer should stop running. It sets keepRunning to false so that the next time the while expression in runClock() is evaluated, it will stop looping.

To use this customized component in a JFrame with other components, another class is defined: SecondCounterLockupMain, shown in Listing 4.2.

Example 4.2. SecondCounterLockupMain.java—The Class Used to Demonstrate SecondCounterLockup

 1: import java.awt.*;
 2: import java.awt.event.*;
 3: import javax.swing.*;
 4: import javax.swing.border.*;
 5:
 6: public class SecondCounterLockupMain extends JPanel {
 7:     private SecondCounterLockup sc;
 8:     private JButton startB;
 9:     private JButton stopB;
10:
11:     public SecondCounterLockupMain() {
12:         sc = new SecondCounterLockup();
13:         startB = new JButton("Start");
14:         stopB = new JButton("Stop");
15:
16:         stopB.setEnabled(false);  // begin with this disabled
17:
18:         startB.addActionListener(new ActionListener() {
19:                 public void actionPerformed(ActionEvent e) {
20:                     // disable to stop more "start" requests
21:                     startB.setEnabled(false);
22:
23:                     // Run the counter. Watch out, trouble here!
24:                     sc.runClock();
25:
26:                     stopB.setEnabled(true);
27:                     stopB.requestFocus();
28:                 }
29:             } );
30:
31:         stopB.addActionListener(new ActionListener() {
32:                 public void actionPerformed(ActionEvent e) {
33:                     stopB.setEnabled(false);
34:                     sc.stopClock();
35:                     startB.setEnabled(true); 
36:                     startB.requestFocus();
37:                 }
38:             } );
39:
40:         JPanel innerButtonP = new JPanel();
41:         innerButtonP.setLayout(new GridLayout(0, 1, 0, 3));
42:         innerButtonP.add(startB);
43:         innerButtonP.add(stopB);
44:
45:         JPanel buttonP = new JPanel();
46:         buttonP.setLayout(new BorderLayout());
47:         buttonP.add(innerButtonP, BorderLayout.NORTH);
48:
49:         this.setLayout(new BorderLayout(10, 10));
50:         this.setBorder(new EmptyBorder(20, 20, 20, 20));
51:         this.add(buttonP, BorderLayout.WEST);
52:         this.add(sc, BorderLayout.CENTER);
53:     }
54:
55:     public static void main(String[] args) {
56:         SecondCounterLockupMain scm = new SecondCounterLockupMain();
57:
58:         JFrame f = new JFrame("Second Counter Lockup");
59:         f.setContentPane(scm);
60:         f.setSize(320, 200);
61:         f.setVisible(true);
62:         f.addWindowListener(new WindowAdapter() {
63:                 public void windowClosing(WindowEvent e) {
64:                     System.exit(0);
65:                 }
66:             } );
67:     }
68: }

In the constructor, a new SecondCounterLockup is created and put into a JPanel with Start and Stop buttons (lines 12–14 and 40–52). In main(), this JPanel subclass, named SecondCounterLockupMain, is put into a JFrame and displayed (lines 55–67). Initially, the Stop button is disabled because the timer is not running (line 16).

When the Start button is pressed, the actionPerformed() method is invoked (lines 19–28) on the anonymous inner subclass of ActionListener (line 18). In there, the Start button is first disabled (line 21) to prevent any further pressing until the timer is stopped. Next, the runClock() method on the SecondCounterLockup object is invoked (line 24). Watch out, here's where the trouble starts! In fact, in this example, none of the other code is ever executed.

Caution

As the name suggests, running SecondCounterLockupMain will not work as intended and locks up the JavaVM. This is harmless and should not stop you from trying the example. When the Start button is pressed, nothing else happens in the application. The Stop button is never enabled. The window exit/close control is ineffective, even though code was written (lines 62–66) to handle this event.

The only way to stop the application is to kill the JavaVM (by going back to the console window and pressing Ctrl+C, or Delete, or whatever mechanism is used on your platform to kill/interrupt/break/terminate a runaway process).

Figure 4.2 shows how this application looks right after startup:

java SecondCounterLockupMain
Just after starting SecondCounterLockupMain.

Figure 4.2. Just after starting SecondCounterLockupMain.

Figure 4.3 shows how it looks after the Start button is pressed (and how it looks until it is killed!). Although the clock variables are being updated internally, the external view never has a chance to be painted. The paint() method is called only one time, when the frame is first drawn, and never displays the changes requested by the repaint() call.

After clicking the Start button.

Figure 4.3. After clicking the Start button.

A clue to the problem can be found in the output on the console:

thread that invoked paint() is AWT-EventQueue-0
thread running runClock() is AWT-EventQueue-0

This shows that the AWT-EventQueue-0 thread is used for both painting and invoking the event handling methods. When the Start button is pressed, the AWT-EventQueue-0 thread invokes the actionPerformed() method (line 19, SecondCounterLockupMain.java). This method in turn invokes runClock(), which continues to loop until keepRunning is set to false. The only way this can be set to false is by the pressing the Stop button. Because the AWT-EventQueue-0 thread is busy in this loop, it never has a chance to invoke the paint() method again, and the display is frozen. No other event can be processed (including the window close event) until the actionPerformed() method returns. But this will never happen! The application is all locked up, spinning in the while loop!

Although this is only an example, this is a very real type of problem. Rather than do any major work in the event handling thread, you should use another thread as the worker and allow the event handling thread to return to the business of handling events.

Tip

GUI event handling code should be relatively brief to allow the event handling thread to return from the handler and prepare to handle the next event. If longer tasks must be performed, the bulk of the work should be passed off to another thread for processing. This helps keep the user interface lively and responsive.

Extending Thread and JComponent?

Using the event handling thread to run the timer proved to be an impossible idea. Another thread has to be used to run the timer. Based on what has been explored in earlier chapters, it would be nice if SecondCounter could inherit from both JComponent and Thread, as illustrated in Figure 4.4.

Inheriting from both JComponent and Thread—impossible in Java!

Figure 4.4. Inheriting from both JComponent and Thread—impossible in Java!

Because multiple inheritance is not permitted in Java, this approach won't work. It is most important that SecondCounter IS-A Component so that it can be added to a Container, like JPanel. How can it also allow a thread to run within it?

Interface java.lang.Runnable

Rather than inherit from Thread, a class can implement the interface java.lang.Runnable to allow a thread to be run within it. Runnable specifies that only one method be implemented:

public void run()

This is the same method signature that run() has in Thread. In fact, Thread also implements Runnable! Note that run() does not take any parameters, does not return anything, and does not declare that it throws any exceptions.

The Runnable interface can be used to get around the lack of multiple inheritance. Figure 4.5 shows SecondCounter extending JComponent and implementing Runnable. SecondCounter IS-A Component and can be added to containers. SecondCounter also IS-A Runnable and can have a new thread begin execution with its run() method.

Getting around the multiple inheritance problem with Runnable.

Figure 4.5. Getting around the multiple inheritance problem with Runnable.

Passing a Runnable Object to a Thread's Constructor

The Thread class has four constructors that take a Runnable object as a parameter:

public Thread(Runnable target)
public Thread(Runnable target, String name)
public Thread(ThreadGroup group, Runnable target)
public Thread(ThreadGroup group, Runnable target, String name)

Any instance of a class that implements the Runnable interface may be passed as the target to one of these constructors. When the Thread instance's start() method is invoked, start() will start the new thread in the run() method of target rather than in Thread's run() method. The Runnable to be used may be specified only at the time of a Thread's construction; the Thread holds a reference to it in a private member variable.

Because SecondCounter now implements Runnable, a new Thread instance should be created with a SecondCounter instance for a target, like this:

SecondCounter sc = new SecondCounter();
Thread t = new Thread(sc);
t.start();

When t.start() is executed, the newly spawned thread will begin execution by invoking the run() method of SecondCounter. Figure 4.6 presents the resulting object diagram. Note that Thread HAS-A reference to a Runnable (which in this case is more specifically a SecondCounter).

The object diagram for a Runnable SecondCounter passed as a target to a Thread constructor.

Figure 4.6. The object diagram for a Runnable SecondCounter passed as a target to a Thread constructor.

Tip

Implementing the Runnable interface, rather than extending Thread, is generally a better choice, even if the class only inherits from Object. This allows you to develop and use general techniques with classes that implement the Runnable interface, without any concern for which particular class this Runnable extended.

Modifying SecondCounter to Use Runnable

Listing 4.3 shows a new version of the timer component that now implements the Runnable interface. The bold text is used to indicate the most notable changes.

Example 4.3. SecondCounterRunnable.java—Implementing the Runnable Interface

 1: import java.awt.*;
 2: import javax.swing.*;
 3: import java.text.*;
 4:
 5: public class SecondCounterRunnable extends JComponent implements Runnable {
 6:     private volatile boolean keepRunning;
 7:     private Font paintFont;
 8:     private volatile String timeMsg;
 9:     private volatile int arcLen;
10:
11:     public SecondCounterRunnable() {
12:         paintFont = new Font("SansSerif", Font.BOLD, 14);
13:         timeMsg = "never started";
14:         arcLen = 0;
15:     }
16:
17:     public void run() {
18:         runClock();
19:     }
20:
21:     public void runClock() {
22:         DecimalFormat fmt = new DecimalFormat("0.000");
23:         long normalSleepTime = 100;
24:
25:         int counter = 0;
26:         keepRunning = true;
27:
28:         while ( keepRunning ) {
29:             try {
30:                 Thread.sleep(normalSleepTime);
31:             } catch ( InterruptedException x ) {
32:                 // ignore
33:             }
34:
35:             counter++;
36:             double counterSecs = counter / 10.0;
37: 
38:             timeMsg = fmt.format(counterSecs);
39:
40:             arcLen = ( ( ( int ) counterSecs ) % 60 ) * 360 / 60;
41:             repaint();
42:         }
43:     }
44:
45:     public void stopClock() {
46:         keepRunning = false;
47:     }
48:
49:     public void paint(Graphics g) {
50:         g.setColor(Color.black);
51:         g.setFont(paintFont);
52:         g.drawString(timeMsg, 0, 15);
53:
54:         g.fillOval(0, 20, 100, 100);  // black border
55:
56:         g.setColor(Color.white);
57:         g.fillOval(3, 23, 94, 94);  // white for unused portion
58:
59:         g.setColor(Color.blue);  // blue for used portion
60:         g.fillArc(2, 22, 96, 96, 90, -arcLen);
61:     }
62: }

Line 5 shows how the new class SecondCounterRunnable is simultaneously both a JComponent and a Runnable:

5: public class SecondCounterRunnable extends JComponent implements Runnable {

This allows it to be added to GUI containers and to have a thread of its own running within it. The requirements of the Runnable interface are met by the run() method (lines 17–19). When the new thread enters run(), it simply invokes the runClock() method where the work of continually updating the component is done. This new thread continues to loop (lines 28–42) every 0.1 seconds until the member variable keepRunning is set to false. This member variable is set to false when another thread (probably the event handling thread) invokes the stopClock() method (lines 45–47).

Note

On lines 6, 8, and 9 of Listing 4.3, the modifier volatile is included for some of the member variables. By indicating that a member variable is volatile, you inform the JavaVM that its value might be changed by one thread while being used by another. In this case, one thread is checking keepRunning, and another thread will change its value to false some time after the timer is started. Under certain circumstances, if the variable was not marked as volatile, the while loop would not see the new value and would run the timer forever. This is an important detail often overlooked (even by experienced developers) and is discussed in detail in Chapter 7, "Concurrent Access to Objects and Variables."

Listing 4.4 shows the code for SecondCounterRunnableMain to work with SecondCounterRunnable.

Example 4.4. SecondCounterRunnableMain.java—Supporting Code to Use SecondCounterRunnable

 1: import java.awt.*;
 2: import java.awt.event.*;
 3: import javax.swing.*;
 4: import javax.swing.border.*;
 5:
 6: public class SecondCounterRunnableMain extends JPanel {
 7:     private SecondCounterRunnable sc;
 8:     private JButton startB;
 9:     private JButton stopB;
10:
11:     public SecondCounterRunnableMain() {
12:         sc = new SecondCounterRunnable();
13:         startB = new JButton("Start");
14:         stopB = new JButton("Stop");
15:
16:         stopB.setEnabled(false);  // begin with this disabled
17:
18:         startB.addActionListener(new ActionListener() {
19:                 public void actionPerformed(ActionEvent e) {
20:                     // disable to stop more "start" requests
21:                     startB.setEnabled(false);
22:
23:                     // thread to run the counter
24:                     Thread counterThread = new Thread(sc, "SecondCounter");
25:                     counterThread.start();
26:
27:                     stopB.setEnabled(true);
28:                     stopB.requestFocus();
29:                 }
30:             } );
31:
32:         stopB.addActionListener(new ActionListener() {
33:                 public void actionPerformed(ActionEvent e) {
34:                     stopB.setEnabled(false); 
35:                     sc.stopClock();
36:                     startB.setEnabled(true);
37:                     startB.requestFocus();
38:                 }
39:             } );
40:
41:         JPanel innerButtonP = new JPanel();
42:         innerButtonP.setLayout(new GridLayout(0, 1, 0, 3));
43:         innerButtonP.add(startB);
44:         innerButtonP.add(stopB);
45:
46:         JPanel buttonP = new JPanel();
47:         buttonP.setLayout(new BorderLayout());
48:         buttonP.add(innerButtonP, BorderLayout.NORTH);
49:
50:         this.setLayout(new BorderLayout(10, 10));
51:         this.setBorder(new EmptyBorder(20, 20, 20, 20));
52:         this.add(buttonP, BorderLayout.WEST);
53:         this.add(sc, BorderLayout.CENTER);
54:     }
55:
56:     public static void main(String[] args) {
57:         SecondCounterRunnableMain scm = new SecondCounterRunnableMain();
58:
59:         JFrame f = new JFrame("Second Counter Runnable");
60:         f.setContentPane(scm);
61:         f.setSize(320, 200);
62:         f.setVisible(true);
63:         f.addWindowListener(new WindowAdapter() {
64:                 public void windowClosing(WindowEvent e) {
65:                     System.exit(0);
66:                 }
67:             } );
68:     }
69: }

Aside from the classname changes from SecondCounterLockup to SecondCounterRunnable, the main difference from the preceding example is what goes on when the Start button is pressed. Just after startup, the application looks the same as before (refer to Figure 4.2), except for the minor detail of the text in the title bar. After the Start button is pressed, the new application looks similar to Figure 4.7.

Approximately 40 seconds after the Start button is pressed while running SecondCounterRunnableMain.

Figure 4.7. Approximately 40 seconds after the Start button is pressed while running SecondCounterRunnableMain.

When the Start button is pressed, the actionPerformed() method is invoked (lines 19–29) on the anonymous inner subclass of ActionListener (line 18). This method is invoked by the JavaVM's event handling thread. In there, the Start button is first disabled (line 21) to prevent any further pressing until the timer is stopped. Next, rather than directly call runClock() and tie up the event handling thread, a new Thread is instantiated using this constructor:

public Thread(Runnable target, String name)

A new Thread is created passing the reference to the SecondCounterRunnable component as the Runnable target for the Thread object to use (line 24). On this same line, a name for the thread is also passed in: SecondCounter. A new thread is spawned by invoking start() on this new Thread object (line 25). This new thread begins its execution asynchronously. The event handling thread proceeds to enable the Stop button (line 27) and requests that it have the focus (line 28). The event handling thread then returns from actionPerformed() and continues on with the business of handling other events as they come up.

Meanwhile, the new thread that was spawned enters the run() method of SecondCounterRunnable and calls runClock(). This newly spawned thread continues to be alive until it finds that keepRunning has been set to false, at which time it returns from runClock(), returns from run(), and dies.

When the Stop button is pressed, the other actionPerformed() method is invoked (lines 33–38) on the anonymous inner subclass of ActionListener (line 32). All event handling methods, including this one, are invoked by the JavaVM's event handling thread. In there, the Stop button is first disabled (line 34) to prevent any further pressing until the timer is restarted. Next, the stopClock() method is invoked on the SecondCounterRunnable object. In there, the keepRunning flag is set to false to signal the while loop to terminate. The event handling thread returns from stopClock() and proceeds to enable the Start button and give it the focus (lines 36 and 37). The event handling thread then returns from actionPerformed() and returns to the JavaVM's event queue to wait for new events to occur.

If the Stop button is pressed after approximately 1 minute and 15 seconds have elapsed, the application looks like Figure 4.8.

Stop was pressed after the timer ran for approximately 75 seconds.

Figure 4.8. Stop was pressed after the timer ran for approximately 75 seconds.

Checking the Accuracy of SecondCounter

During every iteration of the while loop of SecondCounterRunnable, a 100ms sleep is used to achieve the 1/10-second delay between each increment of the counter. What about the time it takes to execute the other statements in the loop? Does this cause the timer to be inaccurate? Probably, but is it significantly inaccurate? To find out, the modified class SecondCounterInaccurate, shown in Listing 4.5, keeps checking the system's real-time clock to find out whether the timer is drifting into inaccuracy by any measurable amount.

Example 4.5. SecondCounterInaccurate.java—Checking the Timer's Accuracy

 1: import java.awt.*;
 2: import javax.swing.*;
 3: import java.text.*;
 4:
 5: public class SecondCounterInaccurate extends JComponent implements Runnable {
 6:     private volatile boolean keepRunning;
 7:     private Font paintFont;
 8:     private volatile String timeMsg;
 9:     private volatile int arcLen;
10:
11:     public SecondCounterInaccurate() {
12:         paintFont = new Font("SansSerif", Font.BOLD, 14);
13:         timeMsg = "never started";
14:         arcLen = 0;
15:     }
16:
17:     public void run() {
18:         runClock();
19:     }
20:
21:     public void runClock() {
22:         DecimalFormat fmt = new DecimalFormat("0.000");
23:         long normalSleepTime = 100;
24:
25:         int counter = 0;
26:         long startTime = System.currentTimeMillis();
27:         keepRunning = true;
28:
29:         while ( keepRunning ) {
30:             try {
31:                 Thread.sleep(normalSleepTime);
32:             } catch ( InterruptedException x ) {
33:                 // ignore
34:             }
35: 
36:             counter++;
37:             double counterSecs = counter / 10.0;
38:             double elapsedSecs =
39:                 ( System.currentTimeMillis() - startTime ) / 1000.0;
40:
41:             double diffSecs = counterSecs - elapsedSecs;
42:
43:             timeMsg = fmt.format(counterSecs) + " - " +
44:                     fmt.format(elapsedSecs) + " = " +
45:                     fmt.format(diffSecs);
46:
47:             arcLen = ( ( ( int ) counterSecs ) % 60 ) * 360 / 60;
48:             repaint();
49:         }
50:     }
51:
52:     public void stopClock() {
53:         keepRunning = false;
54:     }
55:
56:     public void paint(Graphics g) {
57:         g.setColor(Color.black);
58:         g.setFont(paintFont);
59:         g.drawString(timeMsg, 0, 15);
60:
61:         g.fillOval(0, 20, 100, 100);  // black border
62:
63:         g.setColor(Color.white);
64:         g.fillOval(3, 23, 94, 94);  // white for unused portion
65:
66:         g.setColor(Color.blue);  // blue for used portion
67:         g.fillArc(2, 22, 96, 96, 90, -arcLen);
68:     }
69: }

SecondCounterInacurrate is much the same as SecondCounterRunnable, with just a few additions to the runClock() method to measure the real time that has elapsed. In runClock(), before entering the while loop, the current system clock time in milliseconds is captured into a local variable, startTime (line 26). Each time through the loop, the elapsed time in seconds is calculated based on the system clock and stored into the local variable elapsedSecs (lines 38 and 39). The discrepancy in fractional seconds between the real-time system clock and the iteration count is calculated and stored in the local variable diffSecs (line 41). The text message to be drawn in paint() is expanded to include the formatted elapsedSecs and diffSecs values (lines 43–45).

Listing 4.6 shows the code for SecondCounterInaccurateMain. The only differences between this code and the code for SecondCounterRunnableMain are the changes from using SecondCounterRunnable to SecondCounterInaccurate (lines 6, 7, 11, 12, 57, and 59).

Example 4.6. SecondCounterInaccurateMain.java—Code to Use SecondCounterInaccurate

 1: import java.awt.*;
 2: import java.awt.event.*;
 3: import javax.swing.*;
 4: import javax.swing.border.*;
 5:
 6: public class SecondCounterInaccurateMain extends JPanel {
 7:     private SecondCounterInaccurate sc;
 8:     private JButton startB;
 9:     private JButton stopB;
10:
11:     public SecondCounterInaccurateMain() {
12:         sc = new SecondCounterInaccurate();
13:         startB = new JButton("Start");
14:         stopB = new JButton("Stop");
15:
16:         stopB.setEnabled(false);  // begin with this disabled
17:
18:         startB.addActionListener(new ActionListener() {
19:                 public void actionPerformed(ActionEvent e) {
20:                     // disable to stop more "start" requests
21:                     startB.setEnabled(false);
22:
23:                     // thread to run the counter
24:                     Thread counterThread = new Thread(sc, "SecondCounter");
25:                     counterThread.start();
26:
27:                     stopB.setEnabled(true);
28:                     stopB.requestFocus();
29:                 }
30:             } );
31: 
32:         stopB.addActionListener(new ActionListener() {
33:                 public void actionPerformed(ActionEvent e) {
34:                     stopB.setEnabled(false);
35:                     sc.stopClock();
36:                     startB.setEnabled(true);
37:                     startB.requestFocus();
38:                 }
39:             } );
40:
41:         JPanel innerButtonP = new JPanel();
42:         innerButtonP.setLayout(new GridLayout(0, 1, 0, 3));
43:         innerButtonP.add(startB);
44:         innerButtonP.add(stopB);
45:
46:         JPanel buttonP = new JPanel();
47:         buttonP.setLayout(new BorderLayout());
48:         buttonP.add(innerButtonP, BorderLayout.NORTH);
49:
50:         this.setLayout(new BorderLayout(10, 10));
51:         this.setBorder(new EmptyBorder(20, 20, 20, 20));
52:         this.add(buttonP, BorderLayout.WEST);
53:         this.add(sc, BorderLayout.CENTER);
54:     }
55:
56:     public static void main(String[] args) {
57:         SecondCounterInaccurateMain scm = new SecondCounterInaccurateMain();
58:
59:         JFrame f = new JFrame("Second Counter Inaccurate");
60:         f.setContentPane(scm);
61:         f.setSize(320, 200);
62:         f.setVisible(true);
63:         f.addWindowListener(new WindowAdapter() {
64:                 public void windowClosing(WindowEvent e) {
65:                     System.exit(0);
66:                 }
67:             } );
68:     }
69: }

Figure 4.9 presents three snapshots of SecondCounterInaccurateMain running. The first shows that after approximately a minute, the counter is off by 430ms. The second shows that after approximately 2 minutes, the counter is off by almost 1 second. The third shows that after approximately 3 minutes, the gap increased to 1.380 seconds.

The timer shows that it falls a bit more behind each minute.

Figure 4.9. The timer shows that it falls a bit more behind each minute.

Improving the Accuracy of SecondCounter

Although not many statements exist in the while loop, it has been shown that over time, they cause the loop to run significantly more slowly than desired. To improve the accuracy, the sleep time should be varied based on the current system clock time. The final version of SecondCounter is simply called SecondCounter; its code appears in Listing 4.7.

Example 4.7. SecondCounter.java—The Most Accurate Timer

 1: import java.awt.*;
 2: import javax.swing.*;
 3: import java.text.*;
 4:
 5: public class SecondCounter extends JComponent implements Runnable {
 6:     private volatile boolean keepRunning;
 7:     private Font paintFont;
 8:     private volatile String timeMsg;
 9:     private volatile int arcLen;
10:
11:     public SecondCounter() {
12:         paintFont = new Font("SansSerif", Font.BOLD, 14);
13:         timeMsg = "never started";
14:         arcLen = 0;
15:     }
16:
17:     public void run() {
18:         runClock();
19:     }
20:
21:     public void runClock() {
22:         DecimalFormat fmt = new DecimalFormat("0.000");
23:         long normalSleepTime = 100;
24:         long nextSleepTime = normalSleepTime;
25:
26:         int counter = 0;
27:         long startTime = System.currentTimeMillis();
28:         keepRunning = true;
29:
30:         while ( keepRunning ) {
31:             try {
32:                 Thread.sleep(nextSleepTime);
33:             } catch ( InterruptedException x ) {
34:                 // ignore
35:             }
36: 
37:             counter++;
38:             double counterSecs = counter / 10.0;
39:             double elapsedSecs =
40:                 ( System.currentTimeMillis() - startTime ) / 1000.0;
41:
42:             double diffSecs = counterSecs - elapsedSecs;
43:
44:             nextSleepTime = normalSleepTime +
45:                     ( ( long ) ( diffSecs * 1000.0 ) );
46:
47:             if ( nextSleepTime < 0 ) {
48:                 nextSleepTime = 0;
49:             }
50:
51:             timeMsg = fmt.format(counterSecs) + " - " +
52:                     fmt.format(elapsedSecs) + " = " +
53:                     fmt.format(diffSecs);
54:
55:             arcLen = ( ( ( int ) counterSecs ) % 60 ) * 360 / 60;
56:             repaint();
57:         }
58:     }
59:
60:     public void stopClock() {
61:         keepRunning = false;
62:     }
63:
64:     public void paint(Graphics g) {
65:         g.setColor(Color.black);
66:         g.setFont(paintFont);
67:         g.drawString(timeMsg, 0, 15);
68: 
69:         g.fillOval(0, 20, 100, 100);  // black border
70:
71:         g.setColor(Color.white);
72:         g.fillOval(3, 23, 94, 94);  // white for unused portion
73:
74:         g.setColor(Color.blue);  // blue for used portion
75:         g.fillArc(2, 22, 96, 96, 90, -arcLen);
76:     }
77: }

A new local variable named nextSleepTime is used to vary the number of milliseconds to sleep each time through the loop (lines 24 and 32). The nextSleepTime value is recalculated based on the difference between the counter seconds and the system clock seconds (lines 42–45). If this value happens to be less than zero, zero is used instead because it's impossible to sleep for a negative amount of time (lines 47–49).

Listing 4.8 shows the code for SecondCounterMain. The only differences between this code and the code for SecondCounterInaccurateMain are the changes from using SecondCounterInaccurate to SecondCounter (lines 6, 7, 11, 12, 57, and 59).

Example 4.8. SecondCounterMain.java—The Supporting Code for SecondCounter

 1: import java.awt.*;
 2: import java.awt.event.*;
 3: import javax.swing.*;
 4: import javax.swing.border.*;
 5:
 6: public class SecondCounterMain extends JPanel {
 7:     private SecondCounter sc;
 8:     private JButton startB;
 9:     private JButton stopB;
10:
11:     public SecondCounterMain() {
12:         sc = new SecondCounter();
13:         startB = new JButton("Start");
14:         stopB = new JButton("Stop");
15:
16:         stopB.setEnabled(false);  // begin with this disabled
17:
18:         startB.addActionListener(new ActionListener() {
19:                 public void actionPerformed(ActionEvent e) {
20:                     // disable to stop more "start" requests
21:                     startB.setEnabled(false);
22:
23:                     // thread to run the counter
24:                     Thread counterThread = new Thread(sc, "SecondCounter");
25:                     counterThread.start();
26:
27:                     stopB.setEnabled(true);
28:                     stopB.requestFocus();
29:                 }
30:             } );
31: 
32:         stopB.addActionListener(new ActionListener() {
33:                 public void actionPerformed(ActionEvent e) {
34:                     stopB.setEnabled(false);
35:                     sc.stopClock();
36:                     startB.setEnabled(true);
37:                     startB.requestFocus();
38:                 }
39:             } );
40:
41:         JPanel innerButtonP = new JPanel();
42:         innerButtonP.setLayout(new GridLayout(0, 1, 0, 3));
43:         innerButtonP.add(startB);
44:         innerButtonP.add(stopB);
45:
46:         JPanel buttonP = new JPanel();
47:         buttonP.setLayout(new BorderLayout());
48:         buttonP.add(innerButtonP, BorderLayout.NORTH);
49:
50:         this.setLayout(new BorderLayout(10, 10));
51:         this.setBorder(new EmptyBorder(20, 20, 20, 20));
52:         this.add(buttonP, BorderLayout.WEST);
53:         this.add(sc, BorderLayout.CENTER);
54:     }
55:
56:     public static void main(String[] args) {
57:         SecondCounterMain scm = new SecondCounterMain();
58:
59:         JFrame f = new JFrame("Second Counter");
60:         f.setContentPane(scm);
61:         f.setSize(320, 200);
62:         f.setVisible(true);
63:         f.addWindowListener(new WindowAdapter() {
64:                 public void windowClosing(WindowEvent e) {
65:                     System.exit(0);
66:                 }
67:             } );
68:     }
69: }

Figure 4.10 presents three snapshots of the SecondCounterMain application running. The first shows that after approximately a minute, the counter and the actual time are perfectly synchronized. The next snapshot shows that after approximately 2 minutes, the counter is just 40ms ahead. After approximately 3 minutes, the counter time and the actual time are perfectly synchronized again. With this implementation, the SecondCounter continually corrects itself to the system clock, regardless of how quickly or slowly the other code in the loop executes.

The timer now stays very close to the accurate time.

Figure 4.10. The timer now stays very close to the accurate time.

Summary

This chapter shows that extending Thread is not always an option and that a second way to allow a thread to run within a class is to have it implement Runnable. In fact, in most cases, implementing Runnable is preferable to extending Thread.

Here are a few other lessons learned:

  • Do not use the event handling thread to perform long-running tasks; it should be allowed to return to the business of handling events relatively quickly. For the long tasks, use a worker thread. This is critical if a Stop or Cancel Request button exists and could be pressed before the original task completes.

  • Proper use of the volatile keyword is not trivial and is necessary in many cases to guarantee desired code execution behavior when two or more threads access the same member variable. Chapter 7 explains volatile in detail.

  • The amount of time it takes to execute even a few statements is not predictable. This time can become significant if the statements are executed over and over in a loop. When accuracy is important, you should check the system clock to see how much real time actually elapsed and then make adjustments accordingly.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.191.137