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水木清华站∶精华区