Objects provide a way to divide a
program up into independent sections. Often, you also need to turn a program
into separate, independently-running subtasks.
Each of these independent subtasks is
called a thread, and you
program as if each thread runs by itself and has the CPU to itself. Some
underlying mechanism is actually dividing up the CPU time for you, but in
general, you don’t have to think about it, which makes programming with
multiple threads a much easier task.
Some definitions are useful at this
point. A process is a self-contained running
program with its own address space. A
multitasking operating system is capable of
running more than one process (program) at a time, while making it look like
each one is chugging along by periodically providing CPU cycles to each process.
A thread is a single sequential flow of control within a process. A single
process can thus have multiple concurrently executing threads.
There are many possible uses for
multithreading, but in general, you’ll have some part of your program tied
to a particular event or resource, and you don’t want to hang up the rest
of your program because of that. So you create a thread associated with that
event or resource and let it run independently of the main program. A good
example is a “quit” button—you don’t want to be forced
to poll the quit button in every piece of code you write in your program and yet
you want the quit button to be responsive, as if you were checking it
regularly. In fact, one of the most immediately compelling reasons for
multithreading is to produce a responsive user
interface.
As a starting point, consider a program
that performs some CPU-intensive operation and thus ends up ignoring user input
and being unresponsive. This one, a combined applet/application, will simply
display the result of a running counter:
//: c14:Counter1.java // A non-responsive user interface. // <applet code=Counter1 width=300 height=100> // </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class Counter1 extends JApplet { private int count = 0; private JButton onOff = new JButton("Toggle"), start = new JButton("Start"); private JTextField t = new JTextField(10); private boolean runFlag = true; public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); start.addActionListener(new StartL()); cp.add(start); onOff.addActionListener(new OnOffL()); cp.add(onOff); } public void go() { while (true) { try { Thread.sleep(100); } catch (InterruptedException e) {} if (runFlag) t.setText(Integer.toString(count++)); } } class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { go(); } } class OnOffL implements ActionListener { public void actionPerformed(ActionEvent e) { runFlag = !runFlag; } } public static void main(String[] args) { Console.run(new Counter1(), 300, 100); } } ///:~
At this point, the Swing and applet code
should be reasonably familiar from Chapter 13. The go( ) method is
where the program stays busy: it puts the current value of count into the
TextField t, then increments count.
Part of the infinite loop inside
go( ) is to call sleep( ).
sleep( ) must be associated with a
Thread object, and it turns out that every
application has some thread associated with it. (Indeed, Java is based on
threads and there are always some running along with your application.) So
regardless of whether you’re explicitly using threads, you can produce the
current thread used by your program with Thread and the static
sleep( ) method.
Note that sleep( ) can throw
InterruptedException, although throwing such an
exception is considered a hostile way to break from a thread and should be
discouraged. (Once again, exceptions are for exceptional conditions, not normal
flow of control.) Interrupting a sleeping thread is included to support a future
language feature.
When the start button is pressed,
go( ) is invoked. And upon examining go( ), you might
naively think (as I did) that it should allow multithreading because it goes to
sleep. That is, while the method is asleep, it seems like the CPU could be busy
monitoring other button presses. But it turns out that the real problem is that
go( ) never returns, since it’s in an infinite loop, and this
means that actionPerformed( ) never returns. Since you’re
stuck inside actionPerformed( ) for the first keypress, the program
can’t handle any other events. (To get out, you must somehow kill the
process; the easiest way to do this is to press Control-C in the console
window.)
The basic problem here is that
go( ) needs to continue performing its operations, and at the same
time it needs to return so actionPerformed( ) can complete and the
user interface can continue responding to the user. But in a conventional method
like go( ) it cannot continue and at the same time return
control to the rest of the program. This sounds like an impossible thing to
accomplish, as if the CPU must be in two places at once, but this is precisely
the illusion that threading provides. The thread model (and programming support
in Java) is a programming convenience to simplify juggling several operations at
the same time within a single program. With threads, the CPU will pop around and
give each thread some of its time. Each thread has the consciousness of
constantly having the CPU to itself, but the CPU’s time is actually sliced
between all the threads.
Threading
reduces computing efficiency somewhat, but the net improvement in program
design, resource balancing, and user convenience is often quite valuable. Of
course, if you have more than one CPU, then the operating system can dedicate
each CPU to a set of threads or even a single thread and the whole program can
run much faster. Multitasking and multithreading tend to be the most reasonable
ways to utilize multiprocessor
systems.
The simplest way to create a thread is to
inherit from class Thread, which has all the wiring necessary to create
and run threads. The most important method for Thread is
run( ), which you must override to make the thread do your bidding.
Thus, run( ) is the code that will be executed
“simultaneously” with the other threads in a
program.
The following example creates any number
of threads that it keeps track of by assigning each thread a unique number,
generated with a static variable. The
Thread’s run( ) method is
overridden to count down each time it passes through its loop and to finish when
the count is zero (at the point when run( ) returns, the thread is
terminated).
//: c14:SimpleThread.java // Very simple Threading example. public class SimpleThread extends Thread { private int countDown = 5; private static int threadCount = 0; private int threadNumber = ++threadCount; public SimpleThread() { System.out.println("Making " + threadNumber); } public void run() { while(true) { System.out.println("Thread " + threadNumber + "(" + countDown + ")"); if(--countDown == 0) return; } } public static void main(String[] args) { for(int i = 0; i < 5; i++) new SimpleThread().start(); System.out.println("All Threads Started"); } } ///:~
A run( ) method virtually
always has some kind of loop that continues until the thread is no longer
necessary, so you must establish the condition on which to break out of this
loop (or, in the case above, simply return from run( )).
Often, run( ) is cast in the form of an infinite loop, which means
that, barring some external call to stop( ) or
destroy( ) for that thread, it will run forever (until the program
completes).
In main( ) you can see a
number of threads being created and run. The special method that comes with the
Thread class is start( ), which
performs special initialization for the thread and then calls
run( ). So the steps are: the constructor is called to build the
object, then start( ) configures the thread and calls
run( ). If you don’t call start( ) (which you can
do in the constructor, if that’s appropriate) the thread will never be
started.
The output for one run of this program
(it will be different every time) is:
Making 1 Making 2 Making 3 Making 4 Making 5 Thread 1(5) Thread 1(4) Thread 1(3) Thread 1(2) Thread 2(5) Thread 2(4) Thread 2(3) Thread 2(2) Thread 2(1) Thread 1(1) All Threads Started Thread 3(5) Thread 4(5) Thread 4(4) Thread 4(3) Thread 4(2) Thread 4(1) Thread 5(5) Thread 5(4) Thread 5(3) Thread 5(2) Thread 5(1) Thread 3(4) Thread 3(3) Thread 3(2) Thread 3(1)
You’ll notice that nowhere in this
example is sleep( ) called, and yet the output indicates that each
thread gets a portion of the CPU’s time in which to execute. This shows
that sleep( ), while it relies on the existence of a thread in order
to execute, is not involved with either enabling or disabling threading.
It’s simply another method.
You can also see that the
threads are not run in the order that they’re
created. In fact, the order that the CPU attends to an existing set of threads
is indeterminate, unless you go in and adjust the priorities using
Thread’s setPriority( ) method.
When main( ) creates the
Thread objects it isn’t capturing the references for any of them.
An ordinary object would be fair game for garbage collection, but not a
Thread. Each Thread “registers” itself so there is
actually a reference to it someplace and the garbage collector can’t clean
it up.
Now it’s possible to solve the
problem in Counter1.java with a thread. The trick is to place the
subtask—that is, the loop that’s inside
go( )—inside the run( ) method of a thread. When
the user presses the start button, the thread is started, but then the
creation of the thread completes, so even though the thread is running,
the main job of the program (watching for and responding to user-interface
events) can continue. Here’s the solution:
//: c14:Counter2.java // A responsive user interface with threads. // <applet code=Counter2 width=300 height=100> // </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class Counter2 extends JApplet { private class SeparateSubTask extends Thread { int count = 0; boolean runFlag = true; SeparateSubTask() { start(); } public void run() { while (true) { try { sleep(100); } catch (InterruptedException e){} if(runFlag) t.setText(Integer.toString(count++)); } } } private SeparateSubTask sp = null; private JTextField t = new JTextField(10); private JButton onOff = new JButton("Toggle"), start = new JButton("Start"); class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { if(sp == null) sp = new SeparateSubTask(); } } class OnOffL implements ActionListener { public void actionPerformed(ActionEvent e) { if(sp != null) sp.runFlag = !sp.runFlag; // invertFlag(); } } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); start.addActionListener(new StartL()); cp.add(start); onOff.addActionListener(new OnOffL()); cp.add(onOff); } public static void main(String[] args) { Console.run(new Counter2 (), 300, 100); } } ///:~
Counter2 is now a straightforward
program, whose job is only to set up and maintain the user interface. But now,
when the user presses the start button, a method is not called. Instead a
thread of class SeparateSubTask is created (the constructor starts it, in
this case), and then the Counter2 event loop continues. Note that the
reference to the SeparateSubTask is stored so that when you press the
onOff button it can toggle the runFlag inside the
SeparateSubTask object. That thread (when it looks at the flag) can then
start and stop itself. (This could also have been accomplished by making
SeparateSubTask an inner class.)
The class SeparateSubTask is a
simple extension of Thread with a constructor (that stores the
Counter2 reference and then runs the thread by calling
start( )) and a run( ) that essentially contains the
code from inside go( ) in Counter1.java. Because
SeparateSubTask knows that it holds a reference to a Counter2, it
can reach in and access Counter2’s TextField when it needs
to.
When you press the onOff button,
you’ll see a virtually instant response. Of course, the response
isn’t really instant, not like that of a system that’s driven by
interrupts. The counter stops only when the thread has the CPU and notices that
the flag has changed.
You can see that the
inner class
SeparateSubTask is private, which means that its fields and
methods can be given default access (except for run( ), which must
be public since it is public in the base class). The private
inner class is not accessible to anyone but Counter2, and since the
two classes are tightly coupled it’s convenient to loosen the access
restrictions between them. In SeparateSubTask you can see that the
invertFlag( ) method has been removed since Counter2 can now
directly access runFlag.
The reference to the Counter2
object is being captured automatically by the inner class mechanism. In
run( ), you can see that t is transparently accessed, as if
it were a field of SeparateSubTask. The t field in the parent
class is private since SeparateSubTask can access it without
getting any special permission—and it’s always good to make fields
“as private as possible” so they cannot be accidentally
changed by forces outside your class.
Anytime you notice classes that appear to
have high coupling with each other, consider the coding and maintenance
improvements you might get by using inner
classes.
In the example above you can see that the
thread class is separate from the program’s main class. This makes a lot
of sense and is relatively easy to understand. There is, however, an alternate
form that you will often see used that is not so clear but is usually more
concise (which probably accounts for its popularity). This form combines the
main program class with the thread class by making the main program class a
thread. Since for a GUI program the main program class must be inherited from
either Frame or Applet, an interface must be used to paste on the
additional functionality. This interface is called Runnable, and it
contains the same basic method that Thread does. In fact, Thread
also implements Runnable, which specifies only that there be a
run( ) method.
The use of the combined program/thread is
not quite so obvious. When you start the program, you create an object
that’s Runnable, but you don’t start the thread. This must be
done explicitly. You can see this in the following program, which reproduces the
functionality of Counter2:
//: c14:Counter3.java // Using the Runnable interface to turn the // main class into a thread. // <applet code=Counter3 width=300 height=100> // </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class Counter3 extends JApplet implements Runnable { private int count = 0; private boolean runFlag = true; private Thread selfThread = null; private JButton onOff = new JButton("Toggle"), start = new JButton("Start"); private JTextField t = new JTextField(10); public void run() { while (true) { try { selfThread.sleep(100); } catch (InterruptedException e){} if(runFlag) t.setText(Integer.toString(count++)); } } class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { if(selfThread == null) { selfThread = new Thread(Counter3.this); selfThread.start(); } } } class OnOffL implements ActionListener { public void actionPerformed(ActionEvent e) { runFlag = !runFlag; } } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); start.addActionListener(new StartL()); cp.add(start); onOff.addActionListener(new OnOffL()); cp.add(onOff); } public static void main(String[] args) { Console.run(new Counter3(), 300, 100); } } ///:~
Now the run( ) is inside the
class, but it’s still dormant after init( ) completes. When
you press the start button, the thread is created (if it doesn’t
already exist) in the somewhat obscure expression:
new Thread(Counter3.this);
When something has a
Runnable
interface, it simply means that it has a run( ) method, but
there’s nothing special about that—it doesn’t produce any
innate threading abilities, like those of a class inherited from Thread.
So to produce a thread from a Runnable object, you must create a thread
separately and hand it the Runnable object; there’s a special
constructor for this that takes a Runnable as its argument. You can then
call start( ) for that thread:
selfThread.start();
This performs the usual initialization
and then calls run( ).
The convenient aspect about the
Runnable interface is that everything belongs to the same class. If you
need to access something, you simply do it without going through a separate
object. The penalty for this convenience is strict, though—you can have
only a single thread running for that particular object (although you can create
more objects of that type, or create other threads in different
classes).
Note that the Runnable interface
is not what imposes this restriction. It’s the combination of
Runnable and your main class that does it, since you can have only one
object of your main class per
application.
Consider the creation of many different
threads. You can’t do this with the previous example, so you must go back
to having separate classes inherited from Thread to encapsulate the
run( ). But this is a more general solution and easier to
understand, so while the previous example shows a coding style you’ll
often see, I can’t recommend it for most cases because it’s just a
little bit more confusing and less flexible.
The following example repeats the form of
the examples above with counters and toggle buttons. But now all the information
for a particular counter, including the button and text field, is inside its own
object that is inherited from Thread. All the fields in Ticker are
private, which means that the Ticker implementation can be changed
at will, including the quantity and type of data components to acquire and
display information. When a Ticker object is created, the constructor
requires a reference to an AWT Container, which Ticker fills with
its visual components. This way, if you change the visual components, the code
that uses Ticker doesn’t need to be modified.
//: c14:Counter4.java // By keeping your thread as a distinct class, // you can have as many threads as you want. // <applet code=Counter4 width=200 height=600> // <param name=size value="12"></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class Counter4 extends JApplet { private JButton start = new JButton("Start"); private boolean started = false; private Ticker[] s; private boolean isApplet = true; private int size; class Ticker extends Thread { private JButton b = new JButton("Toggle"); private JTextField t = new JTextField(10); private int count = 0; private boolean runFlag = true; public Ticker() { b.addActionListener(new ToggleL()); JPanel p = new JPanel(); p.add(t); p.add(b); // Calls JApplet.getContentPane().add(): getContentPane().add(p); } class ToggleL implements ActionListener { public void actionPerformed(ActionEvent e) { runFlag = !runFlag; } } public void run() { while (true) { if (runFlag) t.setText(Integer.toString(count++)); try { sleep(100); } catch (InterruptedException e) {} } } } class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { if(!started) { started = true; for (int i = 0; i < s.length; i++) s[i].start(); } } } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); // Get parameter "size" from Web page: if (isApplet) size = Integer.parseInt(getParameter("size")); s = new Ticker[size]; for (int i = 0; i < s.length; i++) s[i] = new Ticker(); start.addActionListener(new StartL()); cp.add(start); } public static void main(String[] args) { Counter4 applet = new Counter4(); // This isn't an applet, so set the flag and // produce the parameter values from args: applet.isApplet = false; applet.size = (args.length == 0 ? 12 : Integer.parseInt(args[0])); Console.run(applet, 200, applet.size * 50); } } ///:~
Ticker contains not only its
threading equipment but also the way to control and display the thread. You can
create as many threads as you want without explicitly creating the windowing
components.
In Counter4 there’s an array
of Ticker objects called s. For maximum flexibility, the size of
this array is initialized by reaching out into the Web page using applet
parameters. Here’s what the size parameter looks like on the page,
embedded inside the applet description:
<applet code=Counter4 width=600 height=600> <param name=size value="20"> </applet>
The
param,
name, and
value are all Web-page
keywords. name is what you’ll be referring to in your program, and
value can be any string, not just something that resolves to a
number.
You’ll notice that the
determination of the size of the array s is done inside
init( ), and not as part of an inline definition of s. That
is, you cannot say as part of the class definition (outside of any
methods):
int size = Integer.parseInt(getParameter("size")); Ticker[] s = new Ticker[size];
You can compile this, but you’ll
get a strange “null-pointer exception” at run-time. It works fine if
you move the getParameter( ) initialization inside of
init( ). The applet framework performs the
necessary startup to grab the parameters before entering
init( ).
In addition, this code is set up to be
either an applet or an
application. When it’s an application the
size argument is extracted from the command line (or a default value is
provided).
Once the size of the array is
established, new Ticker objects are created; as part of the Ticker
constructor the button and text field for each Ticker is added to the
applet.
Pressing the start button means
looping through the entire array of Tickers and calling
start( ) for each one. Remember, start( ) performs
necessary thread initialization and then calls run( ) for that
thread.
The ToggleL listener simply
inverts the flag in Ticker and when the associated thread next takes note
it can react accordingly.
One value of this example is that it
allows you to easily create large sets of independent subtasks and to monitor
their behavior. In this case, you’ll see that as the number of subtasks
gets larger, your machine will probably show more divergence in the displayed
numbers because of the way that the threads are served.
You can also experiment to discover how
important the sleep(100) is inside Ticker.run( ). If you
remove the sleep( ), things will work fine until you press a toggle
button. Then that particular thread has a false runFlag and the
run( ) is just tied up in a tight infinite loop, which appears
difficult to break during multithreading, so the responsiveness and speed of the
program really bogs
down.
A “daemon” thread is one that
is supposed to provide a general service in the background as long as the
program is running, but is not part of the essence of the program. Thus, when
all of the non-daemon threads complete the program is terminated. Conversely, if
there are any non-daemon threads still running the program doesn’t
terminate. (There is, for instance, a thread that runs
main( ).)
You can find out if a thread is a daemon
by calling
isDaemon( ), and you
can turn the daemonhood of a thread on and off with
setDaemon( ). If a
thread is a daemon, then any threads it creates will automatically be
daemons.
The following example demonstrates daemon
threads:
//: c14:Daemons.java // Daemonic behavior. import java.io.*; class Daemon extends Thread { private static final int SIZE = 10; private Thread[] t = new Thread[SIZE]; public Daemon() { setDaemon(true); start(); } public void run() { for(int i = 0; i < SIZE; i++) t[i] = new DaemonSpawn(i); for(int i = 0; i < SIZE; i++) System.out.println( "t[" + i + "].isDaemon() = " + t[i].isDaemon()); while(true) yield(); } } class DaemonSpawn extends Thread { public DaemonSpawn(int i) { System.out.println( "DaemonSpawn " + i + " started"); start(); } public void run() { while(true) yield(); } } public class Daemons { public static void main(String[] args) { Thread d = new Daemon(); System.out.println( "d.isDaemon() = " + d.isDaemon()); // Allow the daemon threads to finish // their startup processes: BufferedReader stdin = new BufferedReader( new InputStreamReader(System.in)); System.out.println("Waiting for CR"); try { stdin.readLine(); } catch(IOException e) {} } } ///:~
The Daemon thread sets its daemon
flag to “true” and then spawns a bunch of other threads to show that
they are also daemons. Then it goes into an infinite loop that calls
yield( ) to give up control to the other processes. In an earlier
version of this program, the infinite loops would increment int counters,
but this seemed to bring the whole program to a stop. Using yield( )
makes the program quite peppy.
There’s nothing to keep the program
from terminating once main( ) finishes its job since there are
nothing but daemon threads running. So that you can see the results of starting
all the daemon threads, System.in is set up to read so the program waits
for a carriage return before terminating. Without this you see only some of the
results from the creation of the daemon threads. (Try replacing the
readLine( ) code with sleep( ) calls of various lengths
to see this
behavior.)
You can think of a single-threaded
program as one lonely entity moving around through your problem space and doing
one thing at a time. Because there’s only one entity, you never have to
think about the problem of two entities trying to use the same resource at the
same time, like two people trying to park in the same space, walk through a door
at the same time, or even talk at the same time.
With multithreading, things aren’t
lonely anymore, but you now have the possibility of two or more threads trying
to use the same limited resource at once. Colliding over a resource must be
prevented or else you’ll have two threads trying to access the same bank
account at the same time, print to the same printer, or adjust the same valve,
etc.
Consider a variation on the counters that
have been used so far in this chapter. In the following example, each thread
contains two counters that are incremented and displayed inside
run( ). In addition, there’s another thread of class
Watcher that is watching the counters to see if they’re always
equivalent. This seems like a needless activity, since looking at the code it
appears obvious that the counters will always be the same. But that’s
where the surprise comes in. Here’s the first version of the
program:
//: c14:Sharing1.java // Problems with resource sharing while threading. // <applet code=Sharing1 width=350 height=500> // <param name=size value="12"> // <param name=observers value="15"> // </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class Sharing1 extends JApplet { private static int accessCount = 0; private static JTextField aCount = new JTextField("0", 7); public static void incrementAccess() { accessCount++; aCount.setText(Integer.toString(accessCount)); } private JButton start = new JButton("Start"), observer = new JButton("Observe"); private boolean isApplet = true; private int numCounters = 0; private int numObservers = 0; private TwoCounter[] s; class TwoCounter extends Thread { private boolean started = false; private JTextField t1 = new JTextField(5), t2 = new JTextField(5); private JLabel l = new JLabel("count1 == count2"); private int count1 = 0, count2 = 0; // Add the display components as a panel // to the given container: public TwoCounter() { JPanel p = new JPanel(); p.add(t1); p.add(t2); p.add(l); getContentPane().add(p); } public void start() { if(!started) { started = true; super.start(); } } public void run() { while (true) { t1.setText(Integer.toString(count1++)); t2.setText(Integer.toString(count2++)); try { sleep(500); } catch (InterruptedException e){} } } public void synchTest() { Sharing1.incrementAccess(); if(count1 != count2) l.setText("Unsynched"); } } class Watcher extends Thread { public Watcher() { start(); } public void run() { while(true) { for(int i = 0; i < s.length; i++) s[i].synchTest(); try { sleep(500); } catch (InterruptedException e){} } } } class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { for(int i = 0; i < s.length; i++) s[i].start(); } } class ObserverL implements ActionListener { public void actionPerformed(ActionEvent e) { for(int i = 0; i < numObservers; i++) new Watcher(); } } public void init() { if(isApplet) { numCounters = Integer.parseInt(getParameter("size")); numObservers = Integer.parseInt( getParameter("observers")); } s = new TwoCounter[numCounters]; Container cp = getContentPane(); cp.setLayout(new FlowLayout()); for(int i = 0; i < s.length; i++) s[i] = new TwoCounter(); JPanel p = new JPanel(); start.addActionListener(new StartL()); p.add(start); observer.addActionListener(new ObserverL()); p.add(observer); p.add(new JLabel("Access Count")); p.add(aCount); cp.add(p); } public static void main(String[] args) { Sharing1 applet = new Sharing1(); // This isn't an applet, so set the flag and // produce the parameter values from args: applet.isApplet = false; applet.numCounters = (args.length == 0 ? 12 : Integer.parseInt(args[0])); applet.numObservers = (args.length < 2 ? 15 : Integer.parseInt(args[1])); Console.run(applet, 350, applet.numCounters * 50); } } ///:~
As before, each counter contains its own
display components: two text fields and a label that initially indicates that
the counts are equivalent. These components are added to the Container in
the TwoCounter constructor. Because this thread is started via a button
press by the user, it’s possible that start( ) could be called
more than once. It’s illegal for Thread.start( ) to be called
more than once for a thread (an exception is thrown). You can see that the
machinery to prevent this in the started flag and the overridden
start( ) method.
In run( ), count1 and
count2 are incremented and displayed in a manner that would seem to keep
them identical. Then
sleep( ) is called;
without this call the program balks because it becomes hard for the CPU to swap
tasks.
The synchTest( ) method
performs the apparently useless activity of checking to see if count1 is
equivalent to count2; if they are not equivalent it sets the label to
“Unsynched” to indicate this. But first, it calls a static member of
the class Sharing1 that increments and displays an access counter to show
how many times this check has occurred successfully. (The reason for this will
become apparent in future variations of this example.)
The Watcher class is a thread
whose job is to call synchTest( ) for all of the TwoCounter
objects that are active. It does this by stepping through the array that’s
kept in the Sharing1 object. You can think of the Watcher as
constantly peeking over the shoulders of the TwoCounter
objects.
Sharing1 contains an array of
TwoCounter objects that it initializes in init( ) and starts
as threads when you press the “start” button. Later, when you press
the “Observe” button, one or more observers are created and freed
upon the unsuspecting TwoCounter threads.
Note that to run this as an applet in a
browser, your Web page will need to contain the lines:
<applet code=Sharing1 width=650 height=500> <param name=size value="20"> <param name=observers value="1"> </applet>
You can change the width, height, and
parameters to suit your experimental tastes. By changing the size and
observers you’ll change the behavior of the program. You can also
see that this program is set up to run as a stand-alone application by pulling
the arguments from the command line (or providing defaults).
Here’s the surprising part. In
TwoCounter.run( ), the infinite loop is just repeatedly passing over
the adjacent lines:
t1.setText(Integer.toString(count1++)); t2.setText(Integer.toString(count2++));
(as well as sleeping, but that’s
not important here). When you run the program, however, you’ll discover
that count1 and count2 will be observed (by the Watcher) to
be unequal at times! This is because of the nature of threads—they can be
suspended at any time. So at times, the suspension
occurs between the execution of the above two lines, and the
Watcher thread happens to come along and perform the comparison at just
this moment, thus finding the two counters to be different.
This example shows a fundamental problem
with using threads. You never know when a thread might be run. Imagine sitting
at a table with a fork, about to spear the last piece of food on your plate and
as your fork reaches for it, the food suddenly vanishes (because your thread was
suspended and another thread came in and stole the food). That’s the
problem that you’re dealing with.
Sometimes you don’t care if a
resource is being accessed at the same time you’re trying to use it (the
food is on some other plate). But for multithreading to work, you need some way
to prevent two threads from accessing the same resource, at least during
critical periods.
Preventing this kind of collision is
simply a matter of putting a lock on a resource when one thread is using it. The
first thread that accesses a resource locks it, and then the other threads
cannot access that resource until it is unlocked, at which time another thread
locks and uses it, etc. If the front seat of the car is the limited resource,
the child who shouts “Dibs!” asserts the
lock.
Java has built-in support to prevent
collisions over one kind of resource: the memory in an object. Since you
typically make the data elements of a class
private and access that memory only through
methods, you can prevent collisions by making a particular method
synchronized. Only one thread at a time can call
a synchronized method for a particular object (although that thread can
call more than one of the object’s synchronized methods). Here are simple
synchronized methods:
synchronized void f() { /* ... */ } synchronized void g(){ /* ... */ }
Each object contains a single
lock (also called a
monitor) that is automatically part of the object
(you don’t have to write any special code). When you call any
synchronized method, that object is locked and no other
synchronized method of that object can be called until the first one
finishes and releases the lock. In the example above, if f( ) is
called for an object, g( ) cannot be called for the same object
until f( ) is completed and releases the lock. Thus, there’s a
single lock that’s shared by all the synchronized methods of a
particular object, and this lock prevents common memory from being written by
more than one method at a time (i.e. more than one thread at a
time).
There’s also a single lock per
class (as part of the
Class object for the
class), so that
synchronized
static methods can lock each other out from static data on a
class-wide basis.
Note that if you want to guard some other
resource from simultaneous access by multiple threads, you can do so by forcing
access to that resource through synchronized methods.
Armed with this new keyword it appears
that the solution is at hand: we’ll simply use the synchronized
keyword for the methods in TwoCounter. The following example is the same
as the previous one, with the addition of the new keyword:
//: c14:Sharing2.java // Using the synchronized keyword to prevent // multiple access to a particular resource. // <applet code=Sharing2 width=350 height=500> // <param name=size value="12"> // <param name=observers value="15"> // </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class Sharing2 extends JApplet { TwoCounter[] s; private static int accessCount = 0; private static JTextField aCount = new JTextField("0", 7); public static void incrementAccess() { accessCount++; aCount.setText(Integer.toString(accessCount)); } private JButton start = new JButton("Start"), observer = new JButton("Observe"); private boolean isApplet = true; private int numCounters = 0; private int numObservers = 0; class TwoCounter extends Thread { private boolean started = false; private JTextField t1 = new JTextField(5), t2 = new JTextField(5); private JLabel l = new JLabel("count1 == count2"); private int count1 = 0, count2 = 0; public TwoCounter() { JPanel p = new JPanel(); p.add(t1); p.add(t2); p.add(l); getContentPane().add(p); } public void start() { if(!started) { started = true; super.start(); } } public synchronized void run() { while (true) { t1.setText(Integer.toString(count1++)); t2.setText(Integer.toString(count2++)); try { sleep(500); } catch (InterruptedException e){} } } public synchronized void synchTest() { Sharing2.incrementAccess(); if(count1 != count2) l.setText("Unsynched"); } } class Watcher extends Thread { public Watcher() { start(); } public void run() { while(true) { for(int i = 0; i < s.length; i++) s[i].synchTest(); try { sleep(500); } catch (InterruptedException e){} } } } class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { for(int i = 0; i < s.length; i++) s[i].start(); } } class ObserverL implements ActionListener { public void actionPerformed(ActionEvent e) { for(int i = 0; i < numObservers; i++) new Watcher(); } } public void init() { if(isApplet) { numCounters = Integer.parseInt(getParameter("size")); numObservers = Integer.parseInt( getParameter("observers")); } s = new TwoCounter[numCounters]; Container cp = getContentPane(); cp.setLayout(new FlowLayout()); for(int i = 0; i < s.length; i++) s[i] = new TwoCounter(); JPanel p = new JPanel(); start.addActionListener(new StartL()); p.add(start); observer.addActionListener(new ObserverL()); p.add(observer); p.add(new Label("Access Count")); p.add(aCount); cp.add(p); } public static void main(String[] args) { Sharing2 applet = new Sharing2(); // This isn't an applet, so set the flag and // produce the parameter values from args: applet.isApplet = false; applet.numCounters = (args.length == 0 ? 12 : Integer.parseInt(args[0])); applet.numObservers = (args.length < 2 ? 15 : Integer.parseInt(args[1])); Console.run(applet, 350, applet.numCounters * 50); } } ///:~
You’ll notice that both
run( ) and synchTest( ) are synchronized. If you
synchronize only one of the methods, then the other is free to ignore the object
lock and can be called with impunity. This is an important point: Every method
that accesses a critical shared resource must be synchronized or it
won’t work right.
Now a new issue arises. The
Watcher2 can never get a peek at what’s going on because the entire
run( ) method has been synchronized, and since
run( ) is always running for each object the lock is always tied up
and synchTest( ) can never be called. You can see this because the
accessCount never changes.
What we’d like for this example is
a way to isolate only part of the code inside run( ). The
section of code you want to isolate this way is called a
critical section and you
use the synchronized keyword in a different way to set up a critical
section. Java supports critical sections with the
synchronized block; this time synchronized
is used to specify the object whose lock is being used to synchronize the
enclosed code:
synchronized(syncObject) { // This code can be accessed // by only one thread at a time }
Before the synchronized block can be
entered, the lock must be acquired on syncObject. If some other thread
already has this lock, then the block cannot be entered until the lock is given
up.
The Sharing2 example can be
modified by removing the synchronized keyword from the entire
run( ) method and instead putting a synchronized block around
the two critical lines. But what object should be used as the lock? The one that
is already respected by synchTest( ), which is the current object
(this)! So the modified run( ) looks like
this:
public void run() { while (true) { synchronized(this) { t1.setText(Integer.toString(count1++)); t2.setText(Integer.toString(count2++)); } try { sleep(500); } catch (InterruptedException e){} } }
This is the only change that must be made
to Sharing2.java, and you’ll see that while the two counters are
never out of synch (according to when the Watcher is allowed to look at
them), there is still adequate access provided to the Watcher during the
execution of run( ).
Of course, all synchronization depends on
programmer diligence: every piece of code that can access a shared resource must
be wrapped in an appropriate synchronized block.
Since having two methods write to the
same piece of data never sounds like a particularly good idea, it might
seem to make sense for all methods to be automatically synchronized and
eliminate the synchronized keyword altogether. (Of course, the example
with a synchronized run( ) shows that this wouldn’t work
either.) But it turns out that acquiring a lock is not a cheap
operation—it multiplies the cost of a method call (that is, entering and
exiting from the method, not executing the body of the method) by a minimum of
four times, and could be more depending on your implementation. So if you know
that a particular method will not cause contention problems it is expedient to
leave off the synchronized
keyword.
Now that you understand synchronization
you can take another look at
Java
Beans. Whenever you create a Bean, you must assume that it will run in a
multithreaded environment. This means that:
The first point is
fairly easy to deal with, but the second point requires a little more thought.
Consider the BangBean.java example presented in the last chapter. That
ducked out of the multithreading question by ignoring the synchronized
keyword (which hadn’t been introduced yet) and making the event unicast.
Here’s that example modified to work in a multithreaded environment and to
use multicasting for events:
//: c14:BangBean2.java // You should write your Beans this way so they // can run in a multithreaded environment. import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import java.io.*; import com.bruceeckel.swing.*; public class BangBean2 extends JPanel implements Serializable { private int xm, ym; private int cSize = 20; // Circle size private String text = "Bang!"; private int fontSize = 48; private Color tColor = Color.red; private ArrayList actionListeners = new ArrayList(); public BangBean2() { addMouseListener(new ML()); addMouseMotionListener(new MM()); } public synchronized int getCircleSize() { return cSize; } public synchronized void setCircleSize(int newSize) { cSize = newSize; } public synchronized String getBangText() { return text; } public synchronized void setBangText(String newText) { text = newText; } public synchronized int getFontSize() { return fontSize; } public synchronized void setFontSize(int newSize) { fontSize = newSize; } public synchronized Color getTextColor() { return tColor; } public synchronized void setTextColor(Color newColor) { tColor = newColor; } public void paintComponent(Graphics g) { super.paintComponent(g); g.setColor(Color.black); g.drawOval(xm - cSize/2, ym - cSize/2, cSize, cSize); } // This is a multicast listener, which is // more typically used than the unicast // approach taken in BangBean.java: public synchronized void addActionListener(ActionListener l) { actionListeners.add(l); } public synchronized void removeActionListener(ActionListener l) { actionListeners.remove(l); } // Notice this isn't synchronized: public void notifyListeners() { ActionEvent a = new ActionEvent(BangBean2.this, ActionEvent.ACTION_PERFORMED, null); ArrayList lv = null; // Make a shallow copy of the vector in case // someone adds a listener while we're // calling listeners: synchronized(this) { lv = (ArrayList)actionListeners.clone(); } // Call all the listener methods: for(int i = 0; i < lv.size(); i++) { ActionListener al = (ActionListener)lv.get(i); al.actionPerformed(a); } } class ML extends MouseAdapter { public void mousePressed(MouseEvent e) { Graphics g = getGraphics(); g.setColor(tColor); g.setFont( new Font( "TimesRoman", Font.BOLD, fontSize)); int width = g.getFontMetrics().stringWidth(text); g.drawString(text, (getSize().width - width) /2, getSize().height/2); g.dispose(); notifyListeners(); } } class MM extends MouseMotionAdapter { public void mouseMoved(MouseEvent e) { xm = e.getX(); ym = e.getY(); repaint(); } } public static void main(String[] args) { BangBean2 bb = new BangBean2(); bb.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ System.out.println("ActionEvent" + e); } }); bb.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ System.out.println("BangBean2 action"); } }); bb.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ System.out.println("More action"); } }); Console.run(bb, 300, 300); } } ///:~
Adding synchronized to the methods
is an easy change. However, notice in
addActionListener( ) and
removeActionListener( ) that the
ActionListeners are now added to and removed from an ArrayList, so
you can have as many as you want.
You can see that the method
notifyListeners( ) is not
synchronized. It can be called from more than one thread at a time.
It’s also possible for addActionListener( ) or
removeActionListener( ) to be called in the middle of a call to
notifyListeners( ), which is a problem since it traverses the
ArrayList actionListeners. To alleviate the problem, the ArrayList
is cloned inside a synchronized clause and the clone is traversed. This
way the original ArrayList can be manipulated without impact on
notifyListeners( ).
The paint( ) method is also
not
synchronized.
Deciding whether to synchronize overridden methods is not as clear as when
you’re just adding your own methods. In this example it turns out that
paint( ) seems to work OK whether it’s synchronized or
not. But the issues you must consider are:
The test code in
TestBangBean2 has been modified from that in the previous chapter to
demonstrate the multicast ability of BangBean2 by adding extra
listeners.
The blocked state is the most interesting
and is worth further examination. A thread can become blocked for five reasons:
You can also call
yield( ) (a method
of the Thread class) to voluntarily give up the CPU so that other threads
can run. However, the same thing happens if the scheduler decides that your
thread has had enough time and jumps to another thread. That is, nothing
prevents the scheduler from restarting your thread. When a thread is blocked,
there’s some reason that it cannot continue running.
The following example shows all five ways
of becoming blocked. It all exists in a single file called Blocking.java,
but it will be examined here in discrete pieces. (You’ll notice the
“Continued” and “Continuing” tags that allow the code
extraction tool to piece everything together.) First, the basic
framework:
//: c14:Blocking.java // Demonstrates the various ways a thread // can be blocked. // <applet code=Blocking width=350 height=550> // </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.io.*; import com.bruceeckel.swing.*; //////////// The basic framework /////////// class Blockable extends Thread { private Peeker peeker; protected JTextField state = new JTextField(30); protected int i; public Blockable(Container c) { c.add(state); peeker = new Peeker(this, c); } public synchronized int read() { return i; } protected synchronized void update() { state.setText(getClass().getName() + " state: i = " + i); } public void stopPeeker() { // peeker.stop(); Deprecated in Java 1.2 peeker.terminate(); // The preferred approach } } class Peeker extends Thread { private Blockable b; private int session; private JTextField status = new JTextField(30); private boolean stop = false; public Peeker(Blockable b, Container c) { c.add(status); this.b = b; start(); } public void terminate() { stop = true; } public void run() { while (!stop) { status.setText(b.getClass().getName() + " Peeker " + (++session) + "; value = " + b.read()); try { sleep(100); } catch (InterruptedException e){} } } } ///:Continued
The Blockable class is meant to be
a base class for all the classes in this example that demonstrate blocking. A
Blockable object contains a TextField called state that is
used to display information about the object. The method that displays this
information is update( ). You can see it uses
getClass( ).getName( ) to produce the name of the class instead
of just printing it out; this is because update( ) cannot know the
exact name of the class it is called for, since it will be a class derived from
Blockable.
The indicator of change in
Blockable is an int i, which will be incremented by the
run( ) method of the derived class.
There’s a thread of class
Peeker that is started for each Blockable object, and the
Peeker’s job is to watch its associated Blockable object to
see changes in i by calling read( ) and reporting them in its
status TextField. This is important: Note that read( ) and
update( ) are both synchronized, which means they require
that the object lock be free.
///:Continuing ///////////// Blocking via sleep() /////////// class Sleeper1 extends Blockable { public Sleeper1(Container c) { super(c); } public synchronized void run() { while(true) { i++; update(); try { sleep(1000); } catch (InterruptedException e){} } } } class Sleeper2 extends Blockable { public Sleeper2(Container c) { super(c); } public void run() { while(true) { change(); try { sleep(1000); } catch (InterruptedException e){} } } public synchronized void change() { i++; update(); } } ///:Continued
In Sleeper1 the entire
run( ) method is synchronized. You’ll see that the
Peeker associated with this object will run along merrily until
you start the thread, and then the Peeker stops cold. This is one form of
blocking: since Sleeper1.run( ) is synchronized, and once the
thread starts it’s always inside run( ), the method never
gives up the object lock and the Peeker is blocked.
Sleeper2 provides a solution by
making run un-synchronized. Only the change( ) method is
synchronized, which means that while run( ) is in
sleep( ), the Peeker can access the synchronized
method it needs, namely read( ). Here you’ll see that the
Peeker continues running when you start the Sleeper2
thread.
The next part of the example introduces
the concept of suspension. The Thread class has a method
suspend( ) to
temporarily halt the thread and
resume( ) that
restarts it at the point it was halted. Presumably, resume( ) is
called by some thread outside the suspended one, and in this case there’s
a separate class called Resumer that does just that. Each of the classes
demonstrating suspend/resume has an associated resumer:
///:Continuing /////////// Blocking via suspend() /////////// class SuspendResume extends Blockable { public SuspendResume(Container c) { super(c); new Resumer(this); } } class SuspendResume1 extends SuspendResume { public SuspendResume1(Container c) { super(c);} public synchronized void run() { while(true) { i++; update(); suspend(); // Deprecated in Java 1.2 } } } class SuspendResume2 extends SuspendResume { public SuspendResume2(Container c) { super(c);} public void run() { while(true) { change(); suspend(); // Deprecated in Java 1.2 } } public synchronized void change() { i++; update(); } } class Resumer extends Thread { private SuspendResume sr; public Resumer(SuspendResume sr) { this.sr = sr; start(); } public void run() { while(true) { try { sleep(1000); } catch (InterruptedException e){} sr.resume(); // Deprecated in Java 1.2 } } } ///:Continued
SuspendResume1 also has a
synchronized run( ) method. Again, when you start this thread
you’ll see that its associated Peeker gets blocked waiting for the
lock to become available, which never happens. This is fixed as before in
SuspendResume2, which does not synchronize the entire
run( ) method but instead uses a separate synchronized
change( ) method.
You should be aware that Java
2 deprecates the use of suspend( ) and
resume( ), because suspend( ) holds the object’s
lock and is thus deadlock-prone. That is, you can easily
get a number of locked objects waiting on each other, and this will cause your
program to freeze. Although you might see them used in older programs you should
not use suspend( ) and resume( ). The proper solution is
described later in this chapter.
The point with the first two examples is
that both sleep( ) and suspend( ) do not release
the lock as they are called. You must be aware of this when working with locks.
On the other hand, the method
wait( ) does
release the lock when it is called, which means that other
synchronized methods in the thread object could
be called during a wait( ). In the following two classes,
you’ll see that the run( ) method is fully synchronized
in both cases, however, the Peeker still has full access to the
synchronized methods during a wait( ). This is because
wait( ) releases the lock on the object as it suspends the method
it’s called within.
You’ll also see that there are two
forms of wait( ). The first takes an argument in milliseconds that
has the same meaning as in sleep( ): pause for this period of time.
The difference is that in wait( ), the object lock is released
and you can come out of the wait( ) because of a
notify( ) as well as having the clock run out.
The second form takes no arguments, and
means that the wait( ) will continue until a notify( )
comes along and will not automatically terminate after a time.
One fairly unique aspect of
wait( ) and notify( ) is that both methods are part of
the base class Object and not part of Thread as are
sleep( ), suspend( ), and resume( ).
Although this seems a bit strange at first—to have something that’s
exclusively for threading as part of the universal base class—it’s
essential because they manipulate the lock that’s also part of every
object. As a result, you can put a wait( ) inside any
synchronized method, regardless of whether there’s any threading
going on inside that particular class. In fact, the only place you can
call wait( ) is within a synchronized method or block. If you
call wait( ) or notify( ) within a method that’s
not synchronized, the program will compile, but when you run it
you’ll get an IllegalMonitorStateException
with the somewhat non-intuitive message “current thread not owner.”
Note that sleep( ), suspend( ), and
resume( ) can all be called within non-synchronized methods
since they don’t manipulate the lock.
You can call wait( ) or
notify( ) only for your own lock. Again, you can compile code that
tries to use the wrong lock, but it will produce the same
IllegalMonitorStateException message as before. You can’t fool with
someone else’s lock, but you can ask another object to perform an
operation that manipulates its own lock. So one approach is to create a
synchronized method that calls notify( ) for its own object.
However, in Notifier you’ll see the notify( ) call
inside a synchronized block:
synchronized(wn2) { wn2.notify(); }
where wn2 is the object of type
WaitNotify2. This method, which is not part of WaitNotify2,
acquires the lock on the wn2 object, at which point it’s legal for
it to call notify( ) for wn2 and you won’t get the
IllegalMonitorStateException.
///:Continuing /////////// Blocking via wait() /////////// class WaitNotify1 extends Blockable { public WaitNotify1(Container c) { super(c); } public synchronized void run() { while(true) { i++; update(); try { wait(1000); } catch (InterruptedException e){} } } } class WaitNotify2 extends Blockable { public WaitNotify2(Container c) { super(c); new Notifier(this); } public synchronized void run() { while(true) { i++; update(); try { wait(); } catch (InterruptedException e){} } } } class Notifier extends Thread { private WaitNotify2 wn2; public Notifier(WaitNotify2 wn2) { this.wn2 = wn2; start(); } public void run() { while(true) { try { sleep(2000); } catch (InterruptedException e){} synchronized(wn2) { wn2.notify(); } } } } ///:Continued
wait( ) is typically used
when you’ve gotten to the point where you’re waiting for some other
condition, under the control of forces outside your thread, to change and you
don’t want to idly wait by inside the thread. So wait( )
allows you to put the thread to sleep while waiting for the world to change, and
only when a notify( ) or notifyAll( ) occurs does the
thread wake up and check for changes. Thus, it provides a way to synchronize
between threads.
If a stream is waiting for some IO
activity, it will automatically block. In the following portion of the example,
the two classes work with generic Reader and
Writer objects (using the Java
1.1 Streams), but in the test framework a
piped stream will be set up to
allow the two threads to safely pass data to each other (which is the purpose of
piped streams).
The Sender puts data into the
Writer and sleeps for a random amount of time. However, Receiver
has no sleep( ), suspend( ), or wait( ). But
when it does a read( ) it automatically blocks when there is no more
data.
///:Continuing class Sender extends Blockable { // send private Writer out; public Sender(Container c, Writer out) { super(c); this.out = out; } public void run() { while(true) { for(char c = 'A'; c <= 'z'; c++) { try { i++; out.write(c); state.setText("Sender sent: " + (char)c); sleep((int)(3000 * Math.random())); } catch (InterruptedException e){} catch (IOException e) {} } } } } class Receiver extends Blockable { private Reader in; public Receiver(Container c, Reader in) { super(c); this.in = in; } public void run() { try { while(true) { i++; // Show peeker it's alive // Blocks until characters are there: state.setText("Receiver read: " + (char)in.read()); } } catch(IOException e) { e.printStackTrace();} } } ///:Continued
Both classes also put information into
their state fields and change i so the Peeker can see that
the thread is running.
The main applet class is surprisingly
simple because most of the work has been put into the Blockable
framework. Basically, an array of Blockable objects is created, and since
each one is a thread, they perform their own activities when you press the
“start” button. There’s also a button and
actionPerformed( ) clause to stop all of the Peeker objects,
which provides a demonstration of the alternative to the deprecated (in Java
2) stop( ) method of
Thread.
To set up a connection between the
Sender and Receiver objects, a PipedWriter and
PipedReader are created. Note that the PipedReader in must
be connected to the PipedWriter out via a constructor argument.
After that, anything that’s placed in out can later be extracted
from in, as if it passed through a pipe (hence the name). The in
and out objects are then passed to the Receiver and Sender
constructors, respectively, which treat them as Reader and Writer
objects of any type (that is, they are upcast).
The array of Blockable references
b is not initialized at its point of definition because the piped streams
cannot be set up before that definition takes place (the need for the try
block prevents this).
///:Continuing /////////// Testing Everything /////////// public class Blocking extends JApplet { private JButton start = new JButton("Start"), stopPeekers = new JButton("Stop Peekers"); private boolean started = false; private Blockable[] b; private PipedWriter out; private PipedReader in; class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { if(!started) { started = true; for(int i = 0; i < b.length; i++) b[i].start(); } } } class StopPeekersL implements ActionListener { public void actionPerformed(ActionEvent e) { // Demonstration of the preferred // alternative to Thread.stop(): for(int i = 0; i < b.length; i++) b[i].stopPeeker(); } } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); out = new PipedWriter(); try { in = new PipedReader(out); } catch(IOException e) {} b = new Blockable[] { new Sleeper1(cp), new Sleeper2(cp), new SuspendResume1(cp), new SuspendResume2(cp), new WaitNotify1(cp), new WaitNotify2(cp), new Sender(cp, out), new Receiver(cp, in) }; start.addActionListener(new StartL()); cp.add(start); stopPeekers.addActionListener( new StopPeekersL()); cp.add(stopPeekers); } public static void main(String[] args) { Console.run(new Blocking(), 350, 550); } } ///:~
In init( ), notice the loop
that moves through the entire array and adds the state and
peeker.status text fields to the page.
When the Blockable threads are
initially created, each one automatically creates and starts its own
Peeker. So you’ll see the Peekers running before the
Blockable threads are started. This is essential, as some of the
Peekers will get blocked and stop when the Blockable threads
start, and it’s essential to see this to understand that particular aspect
of
blocking.
Because threads can become blocked
and because objects can have synchronized methods that prevent
threads from accessing that object until the synchronization lock is released,
it’s possible for one thread to get stuck waiting for another thread,
which in turn waits for another thread, etc., until the chain leads back to a
thread waiting on the first one. Thus, there’s a continuous loop of
threads waiting on each other and no one can move. This is called
deadlock. The claim is that it doesn’t happen that often, but when
it happens to you it’s frustrating to debug.
There is no language support to help
prevent deadlock; it’s up to you to avoid it by careful design. These are
not comforting words to the person who’s trying to debug a deadlocking
program.
One change that has been made in Java
2 to reduce the possibility of deadlock is the
deprecation of Thread’s
stop( ),
suspend( ),
resume( ), and
destroy( ) methods.
The reason that the stop( )
method is deprecated is because
it is unsafe. It releases all the locks that the thread had acquired, and if the
objects are in an inconsistent state (“damaged”) other threads can
view and modify them in that state. The resulting problems can be subtle and
difficult to detect. Instead of using stop( ), you should follow the
example in Blocking.java and use a flag to tell the thread when to
terminate itself by exiting its run( ) method.
There are times when a thread blocks,
such as when it is waiting for input, and it cannot poll a flag as it does in
Blocking.java. In these cases, you still shouldn’t use
stop( ), but instead you can use the
interrupt( ) method
in Thread to break out of the blocked
code:
//: c14:Interrupt.java // The alternative approach to using stop() // when a thread is blocked. // <applet code=Interrupt width=200 height=100> // </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; class Blocked extends Thread { public synchronized void run() { try { wait(); // Blocks } catch(InterruptedException e) { System.out.println("InterruptedException"); } System.out.println("Exiting run()"); } } public class Interrupt extends JApplet { private JButton interrupt = new JButton("Interrupt"); private Blocked blocked = new Blocked(); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(interrupt); interrupt.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { System.out.println("Button pressed"); if(blocked == null) return; Thread remove = blocked; blocked = null; // to release it remove.interrupt(); } }); blocked.start(); } public static void main(String[] args) { Console.run(new Interrupt(), 200, 100); } } ///:~
The wait( ) inside
Blocked.run( ) produces the blocked thread. When you press the
button, the blocked reference is set to null so the garbage
collector will clean it up, and then the object’s interrupt( )
method is called. The first time you press the button you’ll see that the
thread quits, but after that there’s no thread to kill so you just see
that the button has been pressed.
The
suspend( ) and resume( ) methods turn out to be
inherently deadlock-prone. When you call suspend( ), the target
thread stops but it still holds any locks that it has acquired up to that point.
So no other thread can access the locked resources until the thread is resumed.
Any thread that wants to resume the target thread and also tries to use any of
the locked resources produces deadlock. You should not use
suspend( ) and resume( ), but instead put a flag in your
Thread class to indicate whether the thread should be active or
suspended. If the flag indicates that the thread is
suspended,
the thread goes into a wait using wait( ). When the flag indicates
that the thread should be resumed the thread is restarted with
notify( ). An example can be produced by modifying
Counter2.java. Although the effect is similar, you’ll notice that
the code organization is quite
different—anonymous
inner classes are used for all of the listeners and the Thread is an
inner class, which makes programming slightly more convenient since it
eliminates some of the extra bookkeeping necessary in
Counter2.java:
//: c14:Suspend.java // The alternative approach to using suspend() // and resume(), which are deprecated in Java 2. // <applet code=Suspend width=300 height=100> // </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class Suspend extends JApplet { private JTextField t = new JTextField(10); private JButton suspend = new JButton("Suspend"), resume = new JButton("Resume"); private Suspendable ss = new Suspendable(); class Suspendable extends Thread { private int count = 0; private boolean suspended = false; public Suspendable() { start(); } public void fauxSuspend() { suspended = true; } public synchronized void fauxResume() { suspended = false; notify(); } public void run() { while (true) { try { sleep(100); synchronized(this) { while(suspended) wait(); } } catch (InterruptedException e){} t.setText(Integer.toString(count++)); } } } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); suspend.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { ss.fauxSuspend(); } }); cp.add(suspend); resume.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { ss.fauxResume(); } }); cp.add(resume); } public static void main(String[] args) { Console.run(new Suspend(), 300, 100); } } ///:~
The flag suspended inside
Suspendable is used to turn suspension on and off. To suspend, the flag
is set to true by calling fauxSuspend( ) and this is detected
inside run( ). The wait( ), as described earlier in this
chapter, must be synchronized so that it has the object lock. In
fauxResume( ), the suspended flag is set to false and
notify( ) is called—since this wakes up wait( )
inside a synchronized clause the fauxResume( ) method must
also be synchronized so that it acquires the lock before calling
notify( ) (thus the lock is available for the wait( ) to
wake up with). If you follow the style shown in this program you can avoid using
suspend( ) and resume( ).
The
destroy( ) method of
Thread has never been implemented; it’s like a
suspend( ) that cannot resume, so it has the same deadlock issues as
suspend( ). However, this is not a deprecated method and it might be
implemented in a future version of Java (after 2) for special situations in
which the risk of a deadlock is acceptable.
You might wonder why these methods, now
deprecated, were included in Java in the first place. It seems a clear admission
of a rather significant mistake to simply remove them outright (and pokes yet
another hole in the arguments for Java’s exceptional design and
infallibility touted by Sun marketing people). The heartening part about the
change is that it clearly indicates that the technical people and not the
marketing people are running the show—they discovered a problem and they
are fixing it. I find this much more promising and hopeful than leaving the
problem in because fixing it would admit an error. It means that Java will
continue to improve, even if it means a little discomfort on the part of Java
programmers. I’d rather deal with the discomfort than watch the language
stagnate.
The
priority of a thread
tells the scheduler how important this thread is. If there are a number of
threads blocked and waiting to be run, the scheduler will run the one with the
highest priority first. However, this doesn’t mean that threads with lower
priority don’t get run (that is, you can’t get deadlocked because of
priorities). Lower priority threads just tend to run less
often.
You can read the priority of a thread
with getPriority( )
and change it with
setPriority( ). The
form of the prior “counter” examples can be used to show the effect
of changing the priorities. In this applet you’ll see that the counters
slow down as the associated threads have their priorities
lowered:
//: c14:Counter5.java // Adjusting the priorities of threads. // <applet code=Counter5 width=450 height=600> // </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; class Ticker2 extends Thread { private JButton b = new JButton("Toggle"), incPriority = new JButton("up"), decPriority = new JButton("down"); private JTextField t = new JTextField(10), pr = new JTextField(3); // Display priority private int count = 0; private boolean runFlag = true; public Ticker2(Container c) { b.addActionListener(new ToggleL()); incPriority.addActionListener(new UpL()); decPriority.addActionListener(new DownL()); JPanel p = new JPanel(); p.add(t); p.add(pr); p.add(b); p.add(incPriority); p.add(decPriority); c.add(p); } class ToggleL implements ActionListener { public void actionPerformed(ActionEvent e) { runFlag = !runFlag; } } class UpL implements ActionListener { public void actionPerformed(ActionEvent e) { int newPriority = getPriority() + 1; if(newPriority > Thread.MAX_PRIORITY) newPriority = Thread.MAX_PRIORITY; setPriority(newPriority); } } class DownL implements ActionListener { public void actionPerformed(ActionEvent e) { int newPriority = getPriority() - 1; if(newPriority < Thread.MIN_PRIORITY) newPriority = Thread.MIN_PRIORITY; setPriority(newPriority); } } public void run() { while (true) { if(runFlag) { t.setText(Integer.toString(count++)); pr.setText( Integer.toString(getPriority())); } yield(); } } } public class Counter5 extends JApplet { private JButton start = new JButton("Start"), upMax = new JButton("Inc Max Priority"), downMax = new JButton("Dec Max Priority"); private boolean started = false; private static final int SIZE = 10; private Ticker2[] s = new Ticker2[SIZE]; private TextField mp = new TextField(3); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); for(int i = 0; i < s.length; i++) s[i] = new Ticker2(cp); cp.add(new JLabel( "MAX_PRIORITY = " + Thread.MAX_PRIORITY)); cp.add(new JLabel("MIN_PRIORITY = " + Thread.MIN_PRIORITY)); cp.add(new JLabel("Group Max Priority = ")); cp.add(mp); cp.add(start); cp.add(upMax); cp.add(downMax); start.addActionListener(new StartL()); upMax.addActionListener(new UpMaxL()); downMax.addActionListener(new DownMaxL()); showMaxPriority(); // Recursively display parent thread groups: ThreadGroup parent = s[0].getThreadGroup().getParent(); while(parent != null) { cp.add(new Label( "Parent threadgroup max priority = " + parent.getMaxPriority())); parent = parent.getParent(); } } public void showMaxPriority() { mp.setText(Integer.toString( s[0].getThreadGroup().getMaxPriority())); } class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { if(!started) { started = true; for(int i = 0; i < s.length; i++) s[i].start(); } } } class UpMaxL implements ActionListener { public void actionPerformed(ActionEvent e) { int maxp = s[0].getThreadGroup().getMaxPriority(); if(++maxp > Thread.MAX_PRIORITY) maxp = Thread.MAX_PRIORITY; s[0].getThreadGroup().setMaxPriority(maxp); showMaxPriority(); } } class DownMaxL implements ActionListener { public void actionPerformed(ActionEvent e) { int maxp = s[0].getThreadGroup().getMaxPriority(); if(--maxp < Thread.MIN_PRIORITY) maxp = Thread.MIN_PRIORITY; s[0].getThreadGroup().setMaxPriority(maxp); showMaxPriority(); } } public static void main(String[] args) { Console.run(new Counter5(), 450, 600); } } ///:~
Ticker2 follows the form
established earlier in this chapter, but there’s an extra TextField
for displaying the priority of the thread and two more buttons for incrementing
and decrementing the priority.
Also notice the use of
yield( ), which voluntarily hands control back to the scheduler.
Without this the multithreading mechanism still works, but you’ll notice
it runs slowly (try removing the call to yield( )!). You could also
call sleep( ), but then the rate of counting would be controlled by
the sleep( ) duration instead of the priority.
The init( ) in
Counter5 creates an array of 10 Ticker2s; their buttons and fields
are placed on the form by the Ticker2 constructor. Counter5 adds
buttons to start everything up as well as increment and decrement the maximum
priority of the threadgroup. In addition, there are labels that display the
maximum and minimum priorities possible for a thread and a TextField to
show the thread group’s maximum priority. (The next section will fully
describe thread groups.) Finally, the priorities of the parent thread groups are
also displayed as labels.
When you press an “up” or
“down” button, that Ticker2’s priority is fetched and
incremented or decremented accordingly.
When you run this program, you’ll
notice several things. First of all, the thread group’s
default priority is 5. Even if
you decrement the maximum priority below 5 before starting the threads (or
before creating the threads, which requires a code change), each thread will
have a default priority of 5.
The simple test is to take one counter
and decrement its priority to one, and observe that it counts much slower. But
now try to increment it again. You can get it back up to the thread
group’s priority, but no higher. Now decrement the thread group’s
priority a couple of times. The thread priorities are unchanged, but if you try
to modify them either up or down you’ll see that they’ll
automatically pop to the priority of the thread group. Also, new threads will
still be given a default priority, even if that’s higher than the group
priority. (Thus the group priority is not a way to prevent new threads from
having higher priorities than existing ones.)
Finally, try to increment the group
maximum priority. It can’t be done. You can only reduce thread group
maximum priorities, not increase
them.
All threads belong to a
thread group. This can be either the default thread
group or a group you explicitly specify when you create the thread. At creation,
the thread is bound to a group and cannot change to a different group. Each
application has at least one thread that belongs to the system thread group. If
you create more threads without specifying a group, they will also belong to the
system thread group.
Thread groups must also belong to other
thread groups. The thread group that a new one belongs to must be specified in
the constructor. If you create a thread group without specifying a thread group
for it to belong to, it will be placed under the system thread group. Thus, all
thread groups in your application will ultimately have the system thread group
as the parent.
The reason for the existence of thread
groups is hard to determine from the literature, which tends to be confusing on
this subject. It’s often cited as “security reasons.”
According to Arnold &
Gosling,[67]
“Threads within a thread group can modify the other threads in the group,
including any farther down the hierarchy. A thread cannot modify threads outside
of its own group or contained groups.” It’s hard to know what
“modify” is supposed to mean here. The following example shows a
thread in a “leaf” subgroup modifying the priorities of all the
threads in its tree of thread groups as well as calling a method for all the
threads in its tree.
//: c14:TestAccess.java // How threads can access other threads // in a parent thread group. public class TestAccess { public static void main(String[] args) { ThreadGroup x = new ThreadGroup("x"), y = new ThreadGroup(x, "y"), z = new ThreadGroup(y, "z"); Thread one = new TestThread1(x, "one"), two = new TestThread2(z, "two"); } } class TestThread1 extends Thread { private int i; TestThread1(ThreadGroup g, String name) { super(g, name); } void f() { i++; // modify this thread System.out.println(getName() + " f()"); } } class TestThread2 extends TestThread1 { TestThread2(ThreadGroup g, String name) { super(g, name); start(); } public void run() { ThreadGroup g = getThreadGroup().getParent().getParent(); g.list(); Thread[] gAll = new Thread[g.activeCount()]; g.enumerate(gAll); for(int i = 0; i < gAll.length; i++) { gAll[i].setPriority(Thread.MIN_PRIORITY); ((TestThread1)gAll[i]).f(); } g.list(); } } ///:~
In main( ), several
ThreadGroups are created, leafing off from each other: x has no
argument but its name (a String), so it is automatically placed in the
“system” thread group, while y is under x and z
is under y. Note that initialization happens in textual order so this
code is legal.
Two threads are created and placed in
different thread groups. TestThread1 doesn’t have a
run( ) method but it does have an f( ) that modifies the
thread and prints something so you can see it was called. TestThread2 is
a subclass of TestThread1 and its run( ) is fairly elaborate.
It first gets the thread group of the current thread, then moves up the heritage
tree by two levels using getParent( ). (This is contrived since I
purposely place the TestThread2 object two levels down in the hierarchy.)
At this point, an array of references to Threads is created using the
method activeCount( ) to ask how many threads are in this thread
group and all the child thread groups. The enumerate( ) method
places references to all of these threads in the array gAll, then I
simply move through the entire array calling the f( ) method for
each thread, as well as modifying the priority. Thus, a thread in a
“leaf” thread group modifies threads in parent thread
groups.
The debugging method list( )
prints all the information about a thread group to standard output and is
helpful when investigating thread group behavior. Here’s the output of the
program:
java.lang.ThreadGroup[name=x,maxpri=10] Thread[one,5,x] java.lang.ThreadGroup[name=y,maxpri=10] java.lang.ThreadGroup[name=z,maxpri=10] Thread[two,5,z] one f() two f() java.lang.ThreadGroup[name=x,maxpri=10] Thread[one,1,x] java.lang.ThreadGroup[name=y,maxpri=10] java.lang.ThreadGroup[name=z,maxpri=10] Thread[two,1,z]
Not only does list( ) print
the class name of ThreadGroup or Thread, but it also prints the
thread group name and its maximum priority. For threads, the thread name is
printed, followed by the thread priority and the group that it belongs to. Note
that list( ) indents the threads and thread groups to indicate that
they are children of the un-indented thread group.
You can see that f( ) is
called by the TestThread2 run( ) method, so it’s
obvious that all threads in a group are vulnerable. However, you can access only
the threads that branch off from your own system thread group tree, and
perhaps this is what is meant by “safety.” You cannot access anyone
else’s system thread group tree.
Putting aside the safety issue, one thing
thread groups do seem to be useful for is control: you can perform certain
operations on an entire thread group with a single command. The following
example demonstrates this and the restrictions on priorities within thread
groups. The commented numbers in parentheses provide a reference to compare to
the output.
//: c14:ThreadGroup1.java // How thread groups control priorities // of the threads inside them. public class ThreadGroup1 { public static void main(String[] args) { // Get the system thread & print its Info: ThreadGroup sys = Thread.currentThread().getThreadGroup(); sys.list(); // (1) // Reduce the system thread group priority: sys.setMaxPriority(Thread.MAX_PRIORITY - 1); // Increase the main thread priority: Thread curr = Thread.currentThread(); curr.setPriority(curr.getPriority() + 1); sys.list(); // (2) // Attempt to set a new group to the max: ThreadGroup g1 = new ThreadGroup("g1"); g1.setMaxPriority(Thread.MAX_PRIORITY); // Attempt to set a new thread to the max: Thread t = new Thread(g1, "A"); t.setPriority(Thread.MAX_PRIORITY); g1.list(); // (3) // Reduce g1's max priority, then attempt // to increase it: g1.setMaxPriority(Thread.MAX_PRIORITY - 2); g1.setMaxPriority(Thread.MAX_PRIORITY); g1.list(); // (4) // Attempt to set a new thread to the max: t = new Thread(g1, "B"); t.setPriority(Thread.MAX_PRIORITY); g1.list(); // (5) // Lower the max priority below the default // thread priority: g1.setMaxPriority(Thread.MIN_PRIORITY + 2); // Look at a new thread's priority before // and after changing it: t = new Thread(g1, "C"); g1.list(); // (6) t.setPriority(t.getPriority() -1); g1.list(); // (7) // Make g2 a child Threadgroup of g1 and // try to increase its priority: ThreadGroup g2 = new ThreadGroup(g1, "g2"); g2.list(); // (8) g2.setMaxPriority(Thread.MAX_PRIORITY); g2.list(); // (9) // Add a bunch of new threads to g2: for (int i = 0; i < 5; i++) new Thread(g2, Integer.toString(i)); // Show information about all threadgroups // and threads: sys.list(); // (10) System.out.println("Starting all threads:"); Thread[] all = new Thread[sys.activeCount()]; sys.enumerate(all); for(int i = 0; i < all.length; i++) if(!all[i].isAlive()) all[i].start(); // Suspends & Stops all threads in // this group and its subgroups: System.out.println("All threads started"); sys.suspend(); // Deprecated in Java 2 // Never gets here... System.out.println("All threads suspended"); sys.stop(); // Deprecated in Java 2 System.out.println("All threads stopped"); } } ///:~
The output that follows has been edited
to allow it to fit on the page (the java.lang. has been removed) and to
add numbers to correspond to the commented numbers in the listing
above.
(1) ThreadGroup[name=system,maxpri=10] Thread[main,5,system] (2) ThreadGroup[name=system,maxpri=9] Thread[main,6,system] (3) ThreadGroup[name=g1,maxpri=9] Thread[A,9,g1] (4) ThreadGroup[name=g1,maxpri=8] Thread[A,9,g1] (5) ThreadGroup[name=g1,maxpri=8] Thread[A,9,g1] Thread[B,8,g1] (6) ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] Thread[C,6,g1] (7) ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] Thread[C,3,g1] (8) ThreadGroup[name=g2,maxpri=3] (9) ThreadGroup[name=g2,maxpri=3] (10)ThreadGroup[name=system,maxpri=9] Thread[main,6,system] ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] Thread[C,3,g1] ThreadGroup[name=g2,maxpri=3] Thread[0,6,g2] Thread[1,6,g2] Thread[2,6,g2] Thread[3,6,g2] Thread[4,6,g2] Starting all threads: All threads started
All programs have at least one thread
running, and the first action in main( ) is to call the
static method of Thread called currentThread( ). From
this thread, the thread group is produced and list( ) is called for
the result. The output is:
(1) ThreadGroup[name=system,maxpri=10] Thread[main,5,system]
You can see that the name of the main
thread group is system, and the name of the main thread is main,
and it belongs to the system thread group.
The second exercise shows that the
system group’s maximum priority can be reduced and the main
thread can have its priority increased:
(2) ThreadGroup[name=system,maxpri=9] Thread[main,6,system]
The third exercise creates a new thread
group, g1, which automatically belongs to the system thread group
since it isn’t otherwise specified. A new thread A is placed in
g1. After attempting to set this group’s maximum priority to the
highest level and A’s priority to the highest level, the result
is:
(3) ThreadGroup[name=g1,maxpri=9] Thread[A,9,g1]
Thus, it’s not possible to change
the thread group’s maximum priority to be higher than its parent thread
group.
The fourth exercise reduces
g1’s maximum priority by two and then tries to increase it up to
Thread.MAX_PRIORITY. The result is:
(4) ThreadGroup[name=g1,maxpri=8] Thread[A,9,g1]
You can see that the increase in maximum
priority didn’t work. You can only decrease a thread group’s maximum
priority, not increase it. Also, notice that thread A’s priority
didn’t change, and now it is higher than the thread group’s maximum
priority. Changing a thread group’s maximum priority doesn’t affect
existing threads.
The fifth exercise attempts to set a new
thread to maximum priority:
(5) ThreadGroup[name=g1,maxpri=8] Thread[A,9,g1] Thread[B,8,g1]
The new thread cannot be changed to
anything higher than the maximum thread group priority.
The default thread priority for this
program is 6; that’s the priority a new thread will be created at and
where it will stay if you don’t manipulate the priority. Exercise six
lowers the maximum thread group priority below the default thread priority to
see what happens when you create a new thread under this
condition:
(6) ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] Thread[C,6,g1]
Even though the maximum priority of the
thread group is 3, the new thread is still created using the default priority of
6. Thus, maximum thread group priority does not affect default priority. (In
fact, there appears to be no way to set the default priority for new
threads.)
After changing the priority, attempting
to decrement it by one, the result is:
(7) ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] Thread[C,3,g1]
Only when you attempt to change the
priority is the thread group’s maximum priority enforced.
A similar experiment is performed in (8)
and (9), in which a new thread group g2 is created as a child of
g1 and its maximum priority is changed. You can see that it’s
impossible for g2’s maximum to go higher than
g1’s:
(8) ThreadGroup[name=g2,maxpri=3] (9) ThreadGroup[name=g2,maxpri=3]
Also notice that g2 is
automatically set to the thread group maximum priority of g1 as g2
is created.
After all of these experiments, the
entire system of thread groups and threads is printed:
(10)ThreadGroup[name=system,maxpri=9] Thread[main,6,system] ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] Thread[C,3,g1] ThreadGroup[name=g2,maxpri=3] Thread[0,6,g2] Thread[1,6,g2] Thread[2,6,g2] Thread[3,6,g2] Thread[4,6,g2]
So because of the rules of thread groups,
a child group must always have a maximum priority that’s less than or
equal to its parent’s maximum priority.
The last part of this program
demonstrates methods for an entire group of threads. First the program moves
through the entire tree of threads and starts each one that hasn’t been
started. For drama, the system group is then suspended and finally
stopped. (Although it’s interesting to see that suspend( ) and
stop( ) work on entire thread groups, you should keep in mind that
these methods are deprecated in Java 2.) But when you
suspend the system group you also suspend the main thread and the
whole program shuts down, so it never gets to the point where the threads are
stopped. Actually, if you do stop the main thread it throws a
ThreadDeath exception, so this is not a typical thing to do. Since
ThreadGroup is inherited from Object, which contains the
wait( ) method, you can also choose to suspend the program for any
number of seconds by calling wait(seconds * 1000). This must acquire the
lock inside a synchronized block, of course.
The ThreadGroup class also has
suspend( ) and resume( ) methods so you can stop and
start an entire thread group and all of its threads and subgroups with a single
command. (Again, suspend( ) and resume( ) are deprecated
in Java 2.)
Thread groups can seem a bit mysterious
at first, but keep in mind that you probably won’t be using them directly
very
often.
Earlier in this chapter, I suggested that
you think carefully before making an applet or main Frame as an
implementation of
Runnable.
If you take that approach, you can make only one of those threads in your
program. This limits your flexibility if you decide that you want to have more
than one thread of that type.
Of course, if you must inherit from a
class and you want to add threading behavior to the class,
Runnable is the correct solution. The final example in this chapter
exploits this by making a Runnable Canvas class that paints
different colors on itself. This application is set up to take values from the
command line to determine how big the grid of colors is and how long to
sleep( ) between color changes. By playing with these values
you’ll discover some interesting and possibly inexplicable features of
threads:
//: c14:ColorBoxes.java // Using the Runnable interface. import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; class CBox extends JPanel implements Runnable { private Thread t; private int pause; private static final Color[] colors = { Color.black, Color.blue, Color.cyan, Color.darkGray, Color.gray, Color.green, Color.lightGray, Color.magenta, Color.orange, Color.pink, Color.red, Color.white, Color.yellow }; private Color cColor = newColor(); private static final Color newColor() { return colors[ (int)(Math.random() * colors.length) ]; } public void paintComponent(Graphics g) { super.paintComponent(g); g.setColor(cColor); Dimension s = getSize(); g.fillRect(0, 0, s.width, s.height); } public CBox(int pause) { this.pause = pause; t = new Thread(this); t.start(); } public void run() { while(true) { cColor = newColor(); repaint(); try { t.sleep(pause); } catch(InterruptedException e) {} } } } public class ColorBoxes extends JFrame { public ColorBoxes(int pause, int grid) { setTitle("ColorBoxes"); Container cp = getContentPane(); cp.setLayout(new GridLayout(grid, grid)); for (int i = 0; i < grid * grid; i++) cp.add(new CBox(pause)); } public static void main(String[] args) { int grid = 8; int pause = 50; if(args.length > 0) grid = Integer.parseInt(args[0]); if(args.length > 1) pause = Integer.parseInt(args[1]); Console.run(new ColorBoxes(pause, grid), 500, 400); } } ///:~
ColorBoxes is a typical
application with a constructor that sets up the GUI. This constructor takes an
argument of int grid to set up the
GridLayout so that it has grid cells in
each dimension. Then it adds the appropriate number of CBox objects to
fill the grid, passing the pause value to each one. In
main( ) you can see how pause and grid have default
values that can be changed if you pass in command-line
arguments.
CBox is where all the work takes
place. This is inherited from Canvas and it
implements the Runnable interface so each Canvas can also be a
Thread. Remember that when you implement Runnable, you don’t
make a Thread object, just a class that has a run( ) method.
Thus, you must explicitly create a Thread object and hand the
Runnable object to the constructor, then call start( ) (this
happens in the constructor). In CBox this thread is called
t.
Notice the array colors, which is
an enumeration of all the colors in class Color. This is used in
newColor( ) to produce a randomly-selected color. The current cell
color is cColor.
paint( ) is quite
simple—it just sets the color to cColor and fills the entire canvas
with that color.
In run( ), you see the
infinite loop that sets the cColor to a new random color and then calls
repaint( ) to show it. Then the thread goes to sleep( )
for the amount of time specified on the command line.
Precisely because this design is flexible
and threading is tied to each Canvas element, you can experiment by
making as many threads as you want. (In reality, there is a restriction imposed
by the number of threads your JVM can comfortably handle.)
This program also makes an interesting
benchmark, since it can show dramatic speed differences between one JVM
implementation and
another.
At some point, you’ll find that
ColorBoxes bogs down. On my machine, this occurred somewhere after a 10 x
10 grid. Why does this happen? You’re naturally suspicious that Swing
might have something to do with it, so here’s an example that tests that
premise by making fewer threads. The code is reorganized so that an
ArrayList implements Runnable and that ArrayList holds a
number of color blocks and randomly chooses ones to update. Then a number of
these ArrayList objects are created, depending roughly on the grid
dimension you choose. As a result, you have far fewer threads than color blocks,
so if there’s a speedup we’ll know it was because there were too
many threads in the previous example:
//: c14:ColorBoxes2.java // Balancing thread use. import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import com.bruceeckel.swing.*; class CBox2 extends JPanel { private static final Color[] colors = { Color.black, Color.blue, Color.cyan, Color.darkGray, Color.gray, Color.green, Color.lightGray, Color.magenta, Color.orange, Color.pink, Color.red, Color.white, Color.yellow }; private Color cColor = newColor(); private static final Color newColor() { return colors[ (int)(Math.random() * colors.length) ]; } void nextColor() { cColor = newColor(); repaint(); } public void paintComponent(Graphics g) { super.paintComponent(g); g.setColor(cColor); Dimension s = getSize(); g.fillRect(0, 0, s.width, s.height); } } class CBoxVector extends ArrayList implements Runnable { private Thread t; private int pause; public CBoxVector(int pause) { this.pause = pause; t = new Thread(this); } public void go() { t.start(); } public void run() { while(true) { int i = (int)(Math.random() * size()); ((CBox2)get(i)).nextColor(); try { t.sleep(pause); } catch(InterruptedException e) {} } } public Object last() { return get(size() - 1);} } public class ColorBoxes2 extends JFrame { private CBoxVector[] v; public ColorBoxes2(int pause, int grid) { setTitle("ColorBoxes2"); Container cp = getContentPane(); cp.setLayout(new GridLayout(grid, grid)); v = new CBoxVector[grid]; for(int i = 0; i < grid; i++) v[i] = new CBoxVector(pause); for (int i = 0; i < grid * grid; i++) { v[i % grid].add(new CBox2()); cp.add((CBox2)v[i % grid].last()); } for(int i = 0; i < grid; i++) v[i].go(); } public static void main(String[] args) { int grid = 8; // Shorter default pause than ColorBoxes: int pause = 5; if(args.length > 0) grid = Integer.parseInt(args[0]); if(args.length > 1) pause = Integer.parseInt(args[1]); Console.run(new ColorBoxes2(pause, grid), 500, 400); } } ///:~
In ColorBoxes2 an array of
CBoxVector is created and initialized to hold grid
CBoxVectors, each of which knows how long to sleep. An equal number of
CBox2 objects is then added to each CBoxVector, and each vector is
told to go( ), which starts its thread.
CBox2 is similar to CBox:
it paints itself with a randomly-chosen color. But that’s all a
CBox2 does. All of the threading has been moved into
CBoxVector.
The CBoxVector could also have
inherited Thread and had a member object of type ArrayList. That
design has the advantage that the add( ) and get( )
methods could then be given specific argument and return value types instead of
generic Objects. (Their names could also be changed to something
shorter.) However, the design used here seemed at first glance to require less
code. In addition, it automatically retains all the other behaviors of an
ArrayList. With all the casting and parentheses necessary for
get( ), this might not be the case as your body of code
grows.
As before, when you implement
Runnable you don’t get all of the equipment that comes with
Thread, so you have to create a new Thread and hand yourself to
its constructor in order to have something to start( ), as you can
see in the CBoxVector constructor and in go( ). The
run( ) method simply chooses a random element number within the
vector and calls nextColor( ) for that element to cause it to choose
a new randomly-selected color.
Upon running this program, you see that
it does indeed run faster and respond more quickly (for instance, when you
interrupt it, it stops more quickly), and it doesn’t seem to bog down as
much at higher grid sizes. Thus, a new factor is added into the threading
equation: you must watch to see that you don’t have “too many
threads” (whatever that turns out to mean for your particular program and
platform). If you do, you must try to use techniques like the one above to
“balance” the number of threads in your program. If you see
performance problems in a multithreaded program you now have a number of issues
to examine:
It is vital to learn when to use
multithreading and when to avoid
it. The main reason to use it is to manage a number of tasks whose intermingling
will make more efficient use of the computer or be more convenient for the user.
The classic example of resource balancing is using the CPU during I/O waits. The
classic example of user convenience is monitoring a “stop” button
during long downloads.
An additional
advantage to threads is that they substitute “light” execution
context switches (of the order of 100 instructions) for “heavy”
process context switches (of the order of 1000s of instructions). Since all
threads in a given process share the same memory space, a light context switch
changes only program execution and local variables. On the other hand, a process
change, the heavy context switch, must exchange the full memory
space.
Threading is like stepping into an
entirely new world and learning a whole new programming language, or at least a
new set of language concepts. With the appearance of thread support in most
microcomputer operating systems, extensions for threads have also been appearing
in programming languages or libraries. In all cases, thread programming (1)
seems mysterious and requires a shift in the way you think about programming and
(2) looks similar to thread support in other languages, so when you understand
threads, you understand a common tongue. And although support for threads can
make Java seem like a more complicated language, don’t blame Java. Threads
are tricky.
One of the biggest difficulties with
threads occurs because more than one thread might be sharing a resource, such as
the memory in an object, and you must make sure that multiple threads
don’t try to read and change that resource at the same time. This requires
judicious use of the synchronized keyword, which is a helpful tool but
must be understood thoroughly because it can quietly introduce deadlock
situations.
In addition, there’s a certain art
to the application of threads. Java is designed to allow you to create as many
objects as you need to solve your problem—at least in theory. (Creating
millions of objects for an engineering finite-element analysis, for example,
might not be practical in Java.) However, it seems that there is an upper bound
to the number of threads you’ll want to create because at some point a
large number of threads seems to become unwieldy. This critical point is not in
the many thousands as it might be with objects, but rather in the neighborhood
of less than 100. As you often create only a handful of threads to solve a
problem, this is typically not much of a limit, yet in a more general design it
becomes a constraint.
A significant non-intuitive issue in
threading is that, because of thread scheduling, you can typically make your
applications run faster by inserting calls to sleep( ) inside
run( )’s main loop. This definitely makes it feel like an art,
in particular when the longer delays seem to speed up performance. Of course,
the reason this happens is that shorter delays can cause the
end-of-sleep( ) scheduler interrupt to happen before the running
thread is ready to go to sleep, forcing the scheduler to stop it and restart it
later so it can finish what it was doing and then go to sleep. It takes extra
thought to realize how messy things can get.
One thing you might notice missing in
this chapter is an animation example, which is one of the most popular things to
do with applets. However, a complete solution (with sound) to this problem comes
with the Java JDK (available at java.sun.com) in the demo section. In
addition, we can expect better animation support to become part of future
versions of Java, while completely different non-Java, non-programming solutions
to animation for the Web are appearing that will probably be superior to
traditional approaches. For explanations about how Java animation works, see
Core Java by Cornell & Horstmann, Prentice-Hall, 1997. For more
advanced discussions of threading, see Concurrent Programming in Java by
Doug Lea, Addison-Wesley, 1997, or Java Threads by Oaks & Wong,
O’Reilly,
1997.
[67]
The Java Programming Language, by Ken Arnold and James Gosling,
Addison-Wesley 1996 pp 179.