Sharing limited resources

Bruce Eckel’s Thinking in Java Contents | Prev | Next

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.

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:

//: Sharing1.java
// Problems with resource sharing while threading
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
 
class TwoCounter extends Thread {
  private boolean started = false;
  private TextField
    t1 = new TextField(5),
    t2 = new TextField(5);
  private Label l =
    new Label("count1 == count2");
  private int count1 = 0, count2 = 0;
  // Add the display components as a panel
  // to the given container:
  public TwoCounter(Container c) {
    Panel p = new Panel();
    p.add(t1);
    p.add(t2);
    p.add(l);
    c.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 {
  private Sharing1 p;
  public Watcher(Sharing1 p) {
    this.p = p;
    start();
  }
  public void run() {
    while(true) {
      for(int i = 0; i < p.s.length; i++)
        p.s[i].synchTest();
      try {
        sleep(500);
      } catch (InterruptedException e){}
    }
  }
}
 
public class Sharing1 extends Applet {
  TwoCounter[] s;
  private static int accessCount = 0;
  private static TextField aCount =
    new TextField("0", 10);
  public static void incrementAccess() {
    accessCount++;
    aCount.setText(Integer.toString(accessCount));
  }
  private Button
    start = new Button("Start"),
    observer = new Button("Observe");
  private boolean isApplet = true;
  private int numCounters = 0;
  private int numObservers = 0;
  public void init() {
    if(isApplet) {
      numCounters =
        Integer.parseInt(getParameter("size"));
      numObservers =
        Integer.parseInt(
          getParameter("observers"));
    }
    s = new TwoCounter[numCounters];
    for(int i = 0; i < s.length; i++)
      s[i] = new TwoCounter(this);
    Panel p = new Panel();
    start.addActionListener(new StartL());
    p.add(start);
    observer.addActionListener(new ObserverL());
    p.add(observer);
    p.add(new Label("Access Count"));
    p.add(aCount);
    add(p);
  }
  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(Sharing1.this);
    }
  }
  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 ? 5 :
        Integer.parseInt(args[0]));
    applet.numObservers =
      (args.length < 2 ? 5 :
        Integer.parseInt(args[1]));
    Frame aFrame = new Frame("Sharing1");
    aFrame.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e){
          System.exit(0);
        }
      });
    aFrame.add(applet, BorderLayout.CENTER);
    aFrame.setSize(350, applet.numCounters *100);
    applet.init();
    applet.start();
    aFrame.setVisible(true);
  }
} ///:~ 

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.

How
Java shares resources

synchronized void f() { /* ... */ }
synchronized void g(){ /* ... */ }

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:

//: Sharing2.java
// Using the synchronized keyword to prevent
// multiple access to a particular resource.
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
 
class TwoCounter2 extends Thread {
  private boolean started = false;
  private TextField
    t1 = new TextField(5),
    t2 = new TextField(5);
  private Label l =
    new Label("count1 == count2");
  private int count1 = 0, count2 = 0;
  public TwoCounter2(Container c) {
    Panel p = new Panel();
    p.add(t1);
    p.add(t2);
    p.add(l);
    c.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 Watcher2 extends Thread {
  private Sharing2 p;
  public Watcher2(Sharing2 p) {
    this.p = p;
    start();
  }
  public void run() {
    while(true) {
      for(int i = 0; i < p.s.length; i++)
        p.s[i].synchTest();
      try {
        sleep(500);
      } catch (InterruptedException e){}
    }
  }
}
 
public class Sharing2 extends Applet {
  TwoCounter2[] s;
  private static int accessCount = 0;
  private static TextField aCount =
    new TextField("0", 10);
  public static void incrementAccess() {
    accessCount++;
    aCount.setText(Integer.toString(accessCount));
  }
  private Button
    start = new Button("Start"),
    observer = new Button("Observe");
  private boolean isApplet = true;
  private int numCounters = 0;
  private int numObservers = 0;
  public void init() {
    if(isApplet) {
      numCounters =
        Integer.parseInt(getParameter("size"));
      numObservers =
        Integer.parseInt(
          getParameter("observers"));
    }
    s = new TwoCounter2[numCounters];
    for(int i = 0; i < s.length; i++)
      s[i] = new TwoCounter2(this);
    Panel p = new Panel();
    start.addActionListener(new StartL());
    p.add(start);
    observer.addActionListener(new ObserverL());
    p.add(observer);
    p.add(new Label("Access Count"));
    p.add(aCount);
    add(p);
  }
  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 Watcher2(Sharing2.this);
    }
  }
  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 ? 5 :
        Integer.parseInt(args[0]));
    applet.numObservers =
      (args.length < 2 ? 5 :
        Integer.parseInt(args[1]));
    Frame aFrame = new Frame("Sharing2");
    aFrame.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e){
          System.exit(0);
        }
      });
    aFrame.add(applet, BorderLayout.CENTER);
    aFrame.setSize(350, applet.numCounters *100);
    applet.init();
    applet.start();
    aFrame.setVisible(true);
  }
} ///:~ 

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.

synchronized(syncObject) {
  // This code can be accessed by only
  // one thread at a time, assuming all
  // threads respect syncObject's lock
}

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.


Synchronized
efficiency

Java
Beans revisited

  1. Whenever
    possible, all the public methods of a Bean should be
    synchronized.
    Of course, this incurs the
    synchronized
    runtime 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.
  2. 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:

//: BangBean2.java
// You should write your Beans this way so they 
// can run in a multithreaded environment.
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.io.*;
 
public class BangBean2 extends Canvas
    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 Vector actionListeners = new Vector();
  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 paint(Graphics 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.addElement(l);
  }
  public synchronized void removeActionListener(
      ActionListener l) {
    actionListeners.removeElement(l);
  }
  // Notice this isn't synchronized:
  public void notifyListeners() {
    ActionEvent a =
      new ActionEvent(BangBean2.this,
        ActionEvent.ACTION_PERFORMED, null);
    Vector lv = null;
    // Make a copy of the vector in case someone
    // adds a listener while we're 
    // calling listeners:
    synchronized(this) {
      lv = (Vector)actionListeners.clone();
    }
    // Call all the listener methods:
    for(int i = 0; i < lv.size(); i++) {
      ActionListener al =
        (ActionListener)lv.elementAt(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();
    }
  }
  // Testing the BangBean2:
  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");
      }
    });
    Frame aFrame = new Frame("BangBean2 Test");
    aFrame.addWindowListener(new WindowAdapter(){
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    });
    aFrame.add(bb, BorderLayout.CENTER);
    aFrame.setSize(300,300);
    aFrame.setVisible(true);
  }
} ///:~ 

  1. 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.
  2. 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,
    it’s best to leave
    paint( )
    un-
    synchronized
    to prevent the extra overhead from the
    synchronized
    method call.
  3. 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.

More by Author

Must Read