Sharing limited resources
Sharing limited resources
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.
Improperly accessing resources
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=watchers 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"),
watcher =
new
JButton("Watch");
private
boolean
isApplet =
true;
private
int
numCounters = 12;
private
int
numWatchers = 15;
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:
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) {
System.err.println("Interrupted");
}
}
}
public
void
synchTest() {
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) {
System.err.println("Interrupted");
}
}
}
}
class
StartL
implements
ActionListener {
public
void
actionPerformed(ActionEvent e) {
for(int
i = 0; i < s.length; i++)
s[i].start();
}
}
class
WatcherL
implements
ActionListener {
public
void
actionPerformed(ActionEvent e) {
for(int
i = 0; i < numWatchers; i++)
new
Watcher();
}
}
public
void
init() {
if(isApplet)
{
String counters =
getParameter("size");
if(counters
!=
null)
numCounters = Integer.parseInt(counters);
String watchers =
getParameter("watchers");
if(watchers
!=
null)
numWatchers = Integer.parseInt(watchers);
}
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);
watcher.addActionListener(new
WatcherL());
p.add(watcher);
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.numWatchers =
(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 content pane of the outer
class object in the TwoCounter constructor.
Because a TwoCounter thread is started via a
keypress 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 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 later 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
“Watch” button, one or more watchers are created and freed upon the
unsuspecting TwoCounter threads.
Note that to run this as an applet in a browser, your
applet tag will need to contain the lines:
<param
name=size
value="20">
<param
name=watchers
value="1">
You can experiment by changing the width, height, and
parameters to suit your tastes. By changing the size and watchers
you’ll change the behavior of the program. 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 Watchers) 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.
How Java shares resources
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 simultaneous access of
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.
Synchronizing the counters
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=watchers 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"),
watcher =
new
JButton("Watch");
private
boolean
isApplet =
true;
private
int
numCounters = 12;
private
int
numWatchers = 15;
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) {
System.err.println("Interrupted");
}
}
}
public
synchronized
void
synchTest() {
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) {
System.err.println("Interrupted");
}
}
}
}
class
StartL
implements
ActionListener {
public
void
actionPerformed(ActionEvent e) {
for(int
i = 0; i < s.length; i++)
s[i].start();
}
}
class
WatcherL
implements
ActionListener {
public
void
actionPerformed(ActionEvent e) {
for(int
i = 0; i < numWatchers; i++)
new
Watcher();
}
}
public
void
init() {
if(isApplet)
{
String counters =
getParameter("size");
if(counters
!=
null)
numCounters = Integer.parseInt(counters);
String watchers =
getParameter("watchers");
if(watchers
!=
null)
numWatchers = Integer.parseInt(watchers);
}
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);
watcher.addActionListener(new
WatcherL());
p.add(watcher);
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.numWatchers =
(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 Watcher 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) {
System.err.println("Interrupted");
}
}
}
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.
Synchronized efficiency
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 much
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. On the other hand, leaving off the
synchronized keyword because you think it is a performance bottleneck,
and hoping that there aren’t any collisions is an invitation to disaster.
JavaBeans revisited
Now that you understand synchronization, you can take
another look at JavaBeans. Whenever you create a Bean, you must assume that it
will run in a multithreaded environment. This means that:
- Whenever possible, all the public methods of a
Bean should be synchronized. Of course, this incurs the
synchronized run-time overhead. If that’s a problem, methods that
will not cause problems in critical sections can be left un-synchronized,
but keep in mind that this is not always obvious. Methods that qualify tend to
be small (such as getCircleSize( ) in the following example) and/or
“atomic,” that is, the method call executes in such a short amount
of code that the object cannot be changed during execution. Making such methods
un-synchronized might not have a significant effect on the execution
speed of your program. You might as well make all public methods of a
Bean synchronized and remove the synchronized keyword only when
you know for sure that it’s necessary and that it makes a difference.
- When firing a multicast event to a bunch of listeners
interested in that event, you must assume that listeners might be added or
removed while moving through the list.
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 List
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)lv.get(i))
.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 (see Appendix A for details of cloning). This way the
original ArrayList can be manipulated without impact on
notifyListeners( ).
The paintComponent( ) 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:
- Does the method modify the state of
“critical” variables within the object? To discover whether the
variables are “critical” you must determine whether they will be
read or set by other threads in the program. (In this case, the reading or
setting is virtually always accomplished via synchronized methods, so you
can just examine those.) In the case of paint( ), no modification
takes place.
- Does the method depend on the state of these
“critical” variables? If a synchronized method modifies a
variable that your method uses, then you might very well want to make your
method synchronized as well. Based on this, you might observe that
cSize is changed by synchronized methods and therefore
paint( ) should be synchronized. Here, however, you can ask
“What’s the worst thing that will happen if cSize is changed
during a paint( )?” When you see that it’s nothing too
bad, and a transient effect at that, you can decide to leave
paint( ) un-synchronized to prevent the extra overhead from
the synchronized method call.
- A third clue is to notice whether the base-class
version of paint( ) is synchronized, which it isn’t.
This isn’t an airtight argument, just a clue. In this case, for example, a
field that is changed via synchronized methods (that is
cSize) has been mixed into the paint( ) formula and might
have changed the situation. Notice, however, that synchronized
doesn’t inherit—that is, if a method is synchronized in the
base class then it is not automatically synchronized in the
derived class overridden version.
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
|