BBS水木清华站∶精华区

发信人: abcd12 (Forgiven Not Forgoten), 信区: Java        
标  题: java thread programming(3) 
发信站: BBS 水木清华站 (Sun Aug 13 22:58:39 2000) 
 
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 de 
fining 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 d 
isplays the time elapsed since it was started. To build this custom componen 
t, at a bare minimum, you must extend Component. Because this example uses S 
wing, you will instead extend JComponent (which indirectly extends Component 
). Figure 4.1 shows the initial class hierarchy for the new customized compo 
nent, 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 secon 
ds to visually reflect the time that has elapsed. 
Listing 4.1 shows the source code for a first cut at defining this class. Th 
is version definitely has serious problems, but it illustrates the necessity 
 of another approach. To keep the evolving versions of SecondCounter straigh 
t, slightly different classnames are used for each version. In this case, th 
e class name is SecondCounterLockup. 
Listing 4.1 SecondCounterLockup.java--The First Attempt at the Timer 
View Code 
This component draws itself with a text message and a circular dial. Initial 
ly, the text message is "never started" (line 13), and the dial is totally w 
hite ( arcLen = 0 , line 14). After the timer is started, the text message i 
ndicates the total elapsed time in fractional seconds. The dial sweeps out a 
 blue-filled arc in a clockwise direction that completes 360 degrees every 6 
0 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 1 
7), 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 i 
s defined to show fractional seconds down to the millisecond (ms). The norma 
lSleepTime is defined as 100ms, which is the 0.1-second interval between upd 
ates 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 co 
ntinue 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). Th 
en, 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 pai 
nt() 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 i 
t should schedule a call to paint() as soon as it can. 
The method stopClock() (lines 44-46) is invoked to signal that the timer sho 
uld stop running. It sets keepRunning to false so that the next time the whi 
le 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. 
Listing 4.2 SecondCounterLockupMain.java--The Class Used to Demonstrate Seco 
ndCounterLockup 
 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 JPan 
el with Start and Stop buttons (lines 12-14 and 40-52). In main(), this JPan 
el subclass, named SecondCounterLockupMain, is put into a JFrame and display 
ed (lines 55-67). Initially, the Stop button is disabled because the timer i 
s not running (line 16). 
When the Start button is pressed, the actionPerformed() method is invoked (l 
ines 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 p 
ressing until the timer is stopped. Next, the runClock() method on the Secon 
dCounterLockup object is invoked (line 24). Watch out, here's where the trou 
ble starts! In fact, in this example, none of the other code is ever execute 
d. 
---------------------------------------------------------------------------- 
---- 
CAUTION: As the name suggests, running SecondCounterLockupMain will not work 
 as intended and locks up the JavaVM. This is harmless and should not stop y 
ou from trying the example. When the Start button is pressed, nothing else h 
appens 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 
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 i 
nternally, the external view never has a chance to be painted. The paint() m 
ethod is called only one time, when the frame is first drawn, and never disp 
lays the changes requested by the repaint() call. 
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 in 
voking the event handling methods. When the Start button is pressed, the AWT 
-EventQueue-0 thread invokes the actionPerformed() method (line 19, SecondCo 
unterLockupMain.java). This method in turn invokes runClock(), which continu 
es to loop until keepRunning is set to false. The only way this can be set t 
o false is by the pressing the Stop button. Because the AWT-EventQueue-0 thr 
ead 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 (includin 
g the window close event) until the actionPerformed() method returns. But th 
is will never happen! The application is all locked up, spinning in the whil 
e loop! 
Although this is only an example, this is a very real type of problem. Rathe 
r than do any major work in the event handling thread, you should use anothe 
r 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 h 
andling thread to return from the handler and prepare to handle the next eve 
nt. 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 li 
vely 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 inher 
it from both JComponent and Thread, as illustrated in Figure 4.4. 
FIGURE 4.4 Inheriting from both JComponent and Thread--impossible in Java! 
Because multiple inheritance is not permitted in Java, this approach won't w 
ork. It is most important that SecondCounter IS-A Component so that it can b 
e added to a Container, like JPanel. How can it also allow a thread to run w 
ithin it? Java uses interfaces to help get around the lack of multiple inher 
itance. 
Interface java.lang.Runnable 
Rather than inherit from Thread, a class can implement the interface java.la 
ng.Runnable to allow a thread to be run within it. Runnable specifies that o 
nly 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 inheri 
tance. Figure 4.5 shows SecondCounter extending JComponent and implementing  
Runnable. SecondCounter IS-A Component and can be added to containers. Secon 
dCounter also IS-A Runnable and can have a new thread begin execution with i 
ts run() method. 
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 para 
meter: 
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 star 
t() method is invoked, start() should start the new thread in the run() meth 
od of target rather than in Thread's run() method. The Runnable to be used m 
ay be specified only at the time of a Thread's construction; the Thread hold 
s 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 resulti 
ng object diagram. Note that Thread HAS-A reference to a Runnable (which in  
this case is more specifically a SecondCounter). 
FIGURE 4.6 The object diagram for a Runnable SecondCounter passed as a targe 
t to a Thread constructor. 
---------------------------------------------------------------------------- 
---- 
TIP: Implementing the Runnable interface, rather than extending Thread, is g 
enerally 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 t 
he Runnable interface. 
Listing 4.3 SecondCounterRunnable.java--Implementing the Runnable Interface 
View Code 
Line 5 shows how the new class SecondCounterRunnable is simultaneously both  
a JComponent and a Runnable: 
View Code 
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 th 
e run() method (lines 17-19). When the new thread enters run(), it simply in 
vokes the runClock() method where the work of continually updating the compo 
nent is done. This new thread continues to loop (lines 28-42) every 0.1 seco 
nds until the member variable keepRunning is set to false. This member varia 
ble 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 v 
olatile, 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 keepRunni 
ng, and another thread will change its value to false some time after the ti 
mer 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 ti 
mer forever. This is an important detail often overlooked (even by experienc 
ed developers) and is discussed in detail in Chapter 7, "Controlling Concurr 
ent Access to an Object." 
---------------------------------------------------------------------------- 
---- 
Listing 4.4 shows the code for SecondCounterRunnableMain to work with Second 
CounterRunnable. 
Listing 4.4 SecondCounterRunnableMain.java--Supporting Code to Use SecondCou 
nterRunnable 
View Code 
Aside from the classname changes from SecondCounterLockup to SecondCounterRu 
nnable, the main difference from the preceding example is what goes on when  
the Start button is pressed. Just after startup, the application looks the s 
ame 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 lo 
oks similar to Figure 4.7. 
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 (l 
ines 19-29) on the anonymous inner subclass of ActionListener (line 18). Thi 
s method is invoked by the JavaVM's event handling thread. In there, the Sta 
rt 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 construct 
or: 
public Thread(Runnable target, String name) 
A new Thread is created passing the reference to the SecondCounterRunnable c 
omponent as the Runnable target for the Thread object to use (line 24). On t 
his 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 thre 
ad proceeds to enable the Stop button (line 27) and requests that it have th 
e focus (line 28). The event handling thread then returns from actionPerform 
ed() and continues on with the business of handling other events as they com 
e up. 
Meanwhile, the new thread that was spawned enters the run() method of Second 
CounterRunnable and calls runClock(). This newly spawned thread continues to 
 be alive until it finds that keepRunning has been set to false, at which ti 
me it returns from runClock(), returns from run(), and dies. 
When the Stop button is pressed, the other actionPerformed() method is invok 
ed (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 3 
4) to prevent any further pressing until the timer is restarted. Next, the s 
topClock() 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 th 
e Start button and give it the focus (lines 36 and 37). The event handling t 
hread 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 ha 
ve elapsed, the application looks like Figure 4.8. 
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 s 
leep 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 signi 
ficantly 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. 
Listing 4.5 SecondCounterInaccurate.java--Checking the Timer's Accuracy 
View Code 
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 c 
lock 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 (li 
nes 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 v 
ariable diffSecs (line 41). The text message to be drawn in paint() is expan 
ded to include the formatted elapsedSecs and diffSecs values (lines 43-45). 
Listing 4.6 shows the code for SecondCounterInaccurateMain. The only differe 
nces between this code and the code for SecondCounterRunnableMain are the ch 
anges from using SecondCounterRunnable to SecondCounterInaccurate (lines 6,  
7, 11, 12, 57, and 59). 
Listing 4.6 SecondCounterInaccurateMain.java--Code to Use SecondCounterInacc 
urate 
View Code 
Figure 4.9 presents three snapshots of SecondCounterInaccurateMain running.  
The first shows that after approximately a minute, the counter is off by 430 
ms. 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. 
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 desire 
d. To improve the accuracy, the sleep time should be varied based on the cur 
rent system clock time. The final version of SecondCounter is simply called  
SecondCounter; its code appears in Listing 4.7. 
Listing 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 milli 
seconds to sleep each time through the loop (lines 24 and 32). The nextSleep 
Time value is recalculated based on the difference between the counter secon 
ds and the system clock seconds (lines 42-45). If this value happens to be l 
ess than zero, zero is used instead because it's impossible to sleep for a n 
egative amount of time (lines 47-49). 
Listing 4.8 shows the code for SecondCounterMain. The only differences betwe 
en this code and the code for SecondCounterInaccurateMain are the changes fr 
om using SecondCounterInaccurate to SecondCounter (lines 6, 7, 11, 12, 57, a 
nd 59). 
Listing 4.8 SecondCounterMain.java--The Supporting Code for SecondCounter. 
View Code 
Figure 4.10 presents three snapshots of the SecondCounterMain application ru 
nning. The first shows that after approximately a minute, the counter and th 
e actual time are perfectly synchronized. The next snapshot shows that after 
 approximately 2 minutes, the counter is just 40ms ahead. After approximatel 
y 3 minutes, the counter time and the actual time are perfectly synchronized 
 again. With this implementation, the SecondCounter continually corrects its 
elf to the system clock, regardless of how quickly or slowly the other code  
in the loop executes. 
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 R 
unnable. In fact, in most cases, implementing Runnable is preferable to exte 
nding Thread. 
Here are a few other lessons learned: 
Do not use the event handling thread to perform long-running tasks; it shoul 
d 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 Can 
cel Request button exists and could be pressed before the original task comp 
letes. 
Proper use of the volatile keyword is not trivial and is necessary in many c 
ases to guarantee desired code execution behavior when two or more threads a 
ccess the same member variable. Chapter 7 explains volatile in detail. 
The amount of time it takes to execute even a few statements is not predicta 
ble. This time can become significant if the statements are executed over an 
d over in a loop. When accuracy is important, you should check the system cl 
ock to see how much real time actually elapsed and then make adjustments acc 
ordingly. 
  
-- 
 
   WE DON'T KNOW WHO WE ARE UNTIL WE SEE WHAT WE DO! 
 
 
※ 来源:·BBS 水木清华站 smth.org·[FROM: 202.204.9.81] 

BBS水木清华站∶精华区