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

Runnable revisited

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:

//: ColorBoxes.java
// Using the Runnable interface
import java.awt.*;
import java.awt.event.*;

class CBox extends Canvas 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 paint(Graphics  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 Frame {
  public ColorBoxes(int pause, int grid) {
    setTitle("ColorBoxes");
    setLayout(new GridLayout(grid, grid));
    for (int i = 0; i < grid * grid; i++)
      add(new CBox(pause));
    addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    });
  }   
  public static void main(String[] args) {
    int pause = 50;
    int grid = 8;
    if(args.length > 0) 
      pause = Integer.parseInt(args[0]);
    if(args.length > 1)
      grid = Integer.parseInt(args[1]);
    Frame f = new ColorBoxes(pause, grid);
    f.setSize(500, 400);
    f.setVisible(true);  
  }
} ///:~ 

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.

Too many threads

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 the AWT 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 a Vector implements Runnable and that Vector holds a number of color blocks and randomly chooses ones to update. Then a number of these Vector 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:

//: ColorBoxes2.java
// Balancing thread use
import java.awt.*;
import java.awt.event.*;
import java.util.*;

class CBox2 extends Canvas {
  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 paint(Graphics  g) {
    g.setColor(cColor);
    Dimension s = getSize();
    g.fillRect(0, 0, s.width, s.height);
  }
}

class CBoxVector 
  extends Vector 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)elementAt(i)).nextColor();
      try {
        t.sleep(pause);
      } catch(InterruptedException e) {}
    } 
  }
}

public class ColorBoxes2 extends Frame {
  private CBoxVector[] v;
  public ColorBoxes2(int pause, int grid) {
    setTitle("ColorBoxes2");
    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].addElement(new CBox2());
      add((CBox2)v[i % grid].lastElement());
    }
    for(int i = 0; i < grid; i++)
      v[i].go();
    addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    });
  }   
  public static void main(String[] args) {
    // Shorter default pause than ColorBoxes:
    int pause = 5;
    int grid = 8;
    if(args.length > 0) 
      pause = Integer.parseInt(args[0]);
    if(args.length > 1)
      grid = Integer.parseInt(args[1]);
    Frame f = new ColorBoxes2(pause, grid);
    f.setSize(500, 400);
    f.setVisible(true);  
  }
} ///:~ 

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 Vector. That design has the advantage that the addElement( ) and elementAt( ) 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 a Vector. With all the casting and parentheses necessary for elementAt( ), 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:

  1. Do you have enough calls to sleep( ), yield( ), and/or wait( )?
  2. Are calls to sleep( ) long enough?
  3. Are you running too many threads?
  4. Have you tried different platforms and JVMs?
Issues like this are one reason that multithreaded programming is often considered an art.

Contents | Prev | Next