Chapter 2. Multithreading

You are probably familiar with multitasking: the ability to have more than one program working at what seems like the same time. For example, you can print while editing or sending a fax. Of course, unless you have a multiple-processor machine, what is really going on is that the operating system is doling out resources to each program, giving the impression of simultaneity. This is possible because while the user may think he or she is keeping the computer busy by, for example, entering data, most of the CPU’s time will be idle. (Afast typist takes around 1/20 of a second per character typed, after all.)

Multitasking can be done in two ways, depending on whether the operating system insists that a program let go or whether the program must cooperate in letting go. The former is called preemptive multitasking; the latter is called cooperative (or, simply, nonpreemptive multitasking). Windows 3.1 is a cooperative multitasking system, and Windows NT (and Windows 95 for 32-bit programs) is preemptive. (Although harder to implement, preemptive multitasking is much more effective. With cooperative multitasking, a badly behaved program can hog everything.)

Multithreaded programs extend the idea of multitasking by taking it one level lower: individual programs will appear to do multiple tasks at the same time. (Each task is usually called a thread—which is short for thread of control. Programs that can run more than thread at once are said to be multithreaded.) Think of each thread as running in a separate context: contexts make it seem as though each thread has its own CPU—with registers, memory, and its own code.

So what is the difference between multiple processes and multiple threads? The essential difference is that while each process has a complete set of its own variables, threads share the same data segment. This sounds somewhat risky, and indeed it can be, as you will see later in this chapter. But it takes much less over-head to create and destroy individual threads than it does to launch new processes, which is why all modern operating systems support multithreading. Moreover, cross-process communication, even when permitted, is much slower than cross-thread communication.

Multithreading is extremely useful in practice: for example, a browser should be able to deal with multiple hosts or to open an e-mail window or to view another page while downloading data. Java itself uses a thread to do garbage collection in the background—thus saving you the trouble of managing memory! This chapter shows you how to add multithreading capability to your Java applications and applets.

NOTE

NOTE

In many programming languages, you have to use an external thread pack-age to do multithreaded programming. Java has multithreading built into the lan-guage, which makes your job much easier.

What Are Threads?

Let us start by looking at a Java program that does not use multiple threads and that, as a consequence, makes it difficult for the user to perform several tasks with that program. After we dissect it, we will then show you how easy it is to have this program run separate threads. This program animates a bouncing ball by continually moving the ball, finding out if it bounces against a wall, and then redrawing it. (See Figure 2-1.)

Using a thread to animate a bouncing ball

Figure 2-1. Using a thread to animate a bouncing ball

As soon as you click on the Start button, the program launches a ball from the upper-left corner of the screen and begins bouncing. The handler of the Start button calls the method bounce() of the Ball class, which contains a loop running through 1,000 moves. After each move, we call the static sleep method of the Thread class to pause the ball for 5 milliseconds.

class Ball 
{  . . . 
   public void bounce() 
   {  draw(); 
      for (int i = 1; i <= 1000; i++) 
      {  move(); 
         try { Thread.sleep(5); } 
         catch(InterruptedException e) {} 
      } 
   } 
}

If you run the program, you can see that the ball bounces around nicely, but it completely takes over the application. If you become tired of the bouncing ball before it has finished its 1,000 bounces and click on the Close button, the ball continues bouncing anyway. You cannot interact with the program until the ball has finished bouncing.

This is not a good situation in theory or in practice, and it is becoming more and more of a problem as networks become more central. After all, when you are reading data over a network connection, it is all too common to be stuck in a time-consuming task that you would really like to interrupt. For example, suppose you download a large image and decide, after seeing a piece of it, that you do not need or want to see the rest; you certainly would like to be able to click on a Stop or Back button to interrupt the loading process. In the next section, we will show you how to keep the user in control by running crucial parts of the code in a separate thread.

Example 2-1 is the entire code for the program.

Example 2-1. Bounce.java

import java.awt.*; 
import java.awt.event.*; 
import corejava.*; 

public class Bounce extends CloseableFrame 
{  public Bounce() 
   {  canvas = new Canvas(); 
      add(canvas, "Center"); 
      Panel p = new Panel(); 
      addButton(p, "Start", 
         new ActionListener() 
         {  public void actionPerformed(ActionEvent evt) 
            {  Ball b = new Ball(canvas); 
               b.bounce(); 
            } 
         }); 

      addButton(p, "Close", 
         new ActionListener() 
         {  public void actionPerformed(ActionEvent evt) 
            {  System.exit(0); 
            } 
         }); 
      add(p, "South"); 
   } 

   public void addButton(Container c, String title, 
      ActionListener a) 
   {  Button b = new Button(title); 
      c.add(b); 
      b.addActionListener(a); 
   } 


   public static void main(String[] args) 
   {  Frame f = new Bounce(); 
      f.show(); 
   } 

   private Canvas canvas; 
} 

class Ball 
{  public Ball(Canvas c) { box = c; } 

   public void draw() 
   {  Graphics g = box.getGraphics(); 
      g.fillOval(x, y, XSIZE, YSIZE); 
      g.dispose(); 
   } 

   public void move() 
   {  Graphics g = box.getGraphics(); 
      g.setXORMode(box.getBackground()); 
      g.fillOval(x, y, XSIZE, YSIZE); 
      x += dx; 
      y += dy; 
      Dimension d = box.getSize(); 
      if (x < 0) 
      { x = 0; dx = -dx; } 
      if (x + XSIZE >= d.width) 
      { x = d.width - XSIZE; dx = -dx; } 
      if (y < 0) 
      { y = 0; dy = -dy; } 
      if (y + YSIZE >= d.height) 
      { y = d.height - YSIZE; dy = -dy; } 
      g.fillOval(x, y, XSIZE, YSIZE); 
      g.dispose(); 
   } 

   public void bounce() 
   {  draw(); 
      for (int i = 1; i <= 1000; i++) 
      {  move(); 
         try { Thread.sleep(5); } 
         catch(InterruptedException e) {} 
      } 
   } 

   private Canvas box; 
   private static final int XSIZE = 10; 
   private static final int YSIZE = 10; 
   private int x = 0; 
   private int y = 0; 
   private int dx = 2; 
   private int dy = 2; 
}

Using Threads to Give Other Tasks a Chance

We will make our bouncing-ball program more responsive by running the code that moves the ball in a separate thread.

NOTE

NOTE

Since most computers do not have multiple processors, Java uses a mechanism in which each thread gets a chance to run for a little while, and then it activates another thread. It also relies on the host operating system to provide the thread scheduling package.

In our next program, we use two threads: one for the bouncing ball and another for the main thread that takes care of the user interface. Because each thread gets a chance to run, the main thread has the opportunity to notice when you click on the Close button while the ball is bouncing. It can then process the “close” action.

There is a simple process for running code in a separate thread in Java: place the code into the run method of a class derived from Thread.

To make our bouncing-ball program into a separate thread, we need only derive Ball from Thread and rename the bounce method run, as in the following code:

class Ball extends Thread 
{  . . . 
   public void run() 
   {  draw(); 
      for (int i = 1; i <= 1000; i++) 
      {  move(); 
         try { sleep(5); } 
         catch(InterruptedException e) {} 
      } 
   } 
}

You may have noticed that we are catching an exception called InterruptedException. Methods like sleep and wait throw this exception when your thread is interrupted because another thread has called the interrupt method. Interrupting a thread is a very drastic way of getting the thread’s attention and should not normally be used. In our programs, we never call interrupt, so we supply no exception handler for it.

Running and Starting Threads

When you construct an object derived from Thread, Java does not automatically call the run method.

Ball b = new Ball(. . .); // won't run yet

You should call the start method in your object to actually start a thread.

b.start();

NOTE

NOTE

Do not call the run method directly—start will call it when the thread is set up and ready to go.

In Java, a thread needs to tell the other threads when it is idle, so the other threads can grab the chance to execute the code in their run procedures. (See Figure 2-2.) The usual way to do this is through the sleep method (sleep is a static method in Thread ). The run method of the Ball class uses the call to sleep(5) to indicate that the thread will be idle for the next 5 milliseconds. After 5 milliseconds, it will start up again, but in the meantime, other threads have a chance to get work done.

The UI and ball threads

Figure 2-2. The UI and ball threads

From a design point of view, it seems strange to have the class Ball extend the class Thread. A ball is an object that moves on the screen and bounces off the corners. Does the is–a rule for inheritance apply here? Is a ball a thread? Not really. Here, we are using inheritance strictly for technical reasons. To get a thread you can control, you need a thread object with a run method. We might as well add that run method to the class whose methods and instance fields the run method uses. Therefore, we make Ball a child class of Thread.

The complete code is shown in Example 2-2.

Example 2-2. BounceThread.java

import corejava.*; 
import java.awt.*; 
import java.awt.event.*; 

public class BounceThread extends CloseableFrame 
{  public BounceThread() 
   {  canvas = new Canvas(); 
      add(canvas, "Center"); 
      Panel p = new Panel(); 
      addButton(p, "Start", 
         new ActionListener() 
         {  public void actionPerformed(ActionEvent evt) 
            {  Ball b = new Ball(canvas); 
               b.start(); 
            } 
         }); 

      addButton(p, "Close", 
         new ActionListener() 
         {  public void actionPerformed(ActionEvent evt) 
            {  canvas.setVisible(false); 
               System.exit(0); 
            } 
         }); 
      add(p, "South"); 
   } 

   public void addButton(Container c, String title, 
      ActionListener a) 
   {  Button b = new Button(title); 
      c.add(b); 
      b.addActionListener(a); 
   } 


   public static void main(String[] args) 
   {  Frame f = new BounceThread(); 
      f.show(); 
   } 

   private Canvas canvas; 
} 

class Ball extends Thread 
{  public Ball(Canvas c) { box = c; } 

   public void draw() 
   {  Graphics g = box.getGraphics(); 
      g.fillOval(x, y, XSIZE, YSIZE); 
      g.dispose(); 
   } 

   public void move() 
   {  if (!box.isVisible()) return; 
      Graphics g = box.getGraphics(); 
      g.setXORMode(box.getBackground()); 
      g.fillOval(x, y, XSIZE, YSIZE); 
      x += dx; 
      y += dy; 
      Dimension d = box.getSize(); 
      if (x < 0) 
      { x = 0; dx = -dx; } 
      if (x + XSIZE >= d.width) 
      { x = d.width - XSIZE; dx = -dx; } 
      if (y < 0) 
      { y = 0; dy = -dy; } 
      if (y + YSIZE >= d.height) 
      { y = d.height - YSIZE; dy = -dy; } 
      g.fillOval(x, y, XSIZE, YSIZE); 
      g.dispose(); 
   } 

   public void run() 
   {  draw(); 
      for (int i = 1; i <= 1000; i++) 
      {  move(); 
         try { Thread.sleep(5); } catch(InterruptedException e) {} 
      } 
   } 

   private Canvas box; 
   private static final int XSIZE = 10; 
   private static final int YSIZE = 10; 
   private int x = 0; 
   private int y = 0; 
   private int dx = 2; 
   private int dy = 2; 
}

Running Multiple Threads

Run the program in the preceding section. Now, click on the Start button again while a ball is running. Click on it a few more times. You will see a whole bunch of balls bouncing away as captured in Figure 2-3. Each of them will move 1,000 times until it comes to its final resting place.

Multiple threads

Figure 2-3. Multiple threads

This is a great advantage of the Java thread architecture. It is very easy to create any number of autonomous objects that appear to run in parallel.

You can enumerate the currently running threads—see the API note in the “Thread Groups” section.

Thread Properties

Thread States

Threads can be in one of four states:

  • new

  • runnable

  • blocked

  • dead

Each of these states is explained as follows:

New threads

When you create a thread with the new operator—for example, new Ball() —the thread is not yet running. This means that it is in the new state. When a thread is in the new state, Java has not started executing code inside of it. A certain amount of bookkeeping needs to be done before a thread can run. Doing the bookkeeping and determining the needed memory allocation are the tasks of the start method.

Runnable threads

Once you invoke the start method, the thread is runnable. A runnable thread may not yet be running. It is up to the operating system to give it time to do so. When the code inside the thread begins executing, the thread is running. (The Java documentation does not call this a separate state, though. A running thread is still in the runnable state.)

How this happens is up to the operating system. The thread package in Java needs to work with the underlying operating system. Only the operating system can provide the CPU cycles. The so-called green threads package that is used by Java 1.x on Solaris, for example, keeps a running thread active until a higher-priority thread awakes and takes control. Other thread systems (such as Windows 95 and Windows NT) give each runnable thread a slice of time to perform its task. When that slice of time is exhausted, the operating system gives another thread an opportunity to work. This approach is more sophisticated and makes better use of the multithreading capabilities of Java. Future releases of Java on Solaris are expected to allow use of the native Solaris threads, which also perform time-slicing.

NOTE

NOTE

As we write this, Sun has released an early version of the native thread package for Solaris. Check out http://java.sun.com/products/jdk/1.1/index.html#techpacks.

Always keep in mind that a runnable thread may or may not be running at any given time. See Figure 2-4. (This is why the state is called “runnable” and not “running.”)

Time-slicing on a single CPU

Figure 2-4. Time-slicing on a single CPU

Blocked threads

A thread enters the blocked state when one of the following four actions occurs:

  1. Someone calls the sleep() method of the thread.

  2. Someone calls the suspend() method of the thread.

  3. The thread calls the wait() method.

  4. The thread calls an operation that is blocking on input/output, that is, an operation that will not return to its caller until input and output operations are complete.

Figure 2-5 shows the states that a thread can have and the possible transitions from one state to another. When a thread is blocked (or, of course, when it dies), another thread is scheduled to run. When a blocked thread is reactivated (for example, because it has slept the required number of milliseconds or because the I/O it waited for is complete), the scheduler checks to see if it has a higher priority than the currently running thread. If so, it preempts the current thread and starts running the blocked thread again. (On a machine with multiple processors, each processor runs a separate thread.)

Thread states

Figure 2-5. Thread states

For example, the run method of the Ball thread puts itself to sleep for 5 milliseconds after it has completed a move.

class Ball extends Thread 
{  . . . 
   public void run() 
   {  draw(); 
      for (int i = 1; i <= 1000; i++) 
      {  move(); 
         try {sleep(5); } 
         catch(InterruptedException e) {} 
      } 
   } 
}

This gives other threads (in our case, other balls and the main thread) the chance to run. If the computer has multiple processors, then more than one thread has a chance to run at the same time.

Moving Out of a Blocked State

The thread must move out of the blocked state and back into the runnable state, using the opposite of the route that put it into the blocked state.

  1. If a thread has been put to sleep, the specified number of milliseconds must expire.

  2. If a thread has been suspended, then someone must call its resume method.

  3. If a thread called wait, then the owner of the monitor on whose availability the thread is waiting must call notify or notifyAll. (We cover monitors later in this chapter.)

  4. If a thread is waiting for the completion of an input or output operation, then the operation must have finished.

NOTE

NOTE

You cannot activate a blocked thread unless you use the same route that blocked it in the first place.

For example, if a thread is blocked on input and you call its resume method, it stays blocked.

If you invoke a method on a thread that is incompatible with its state, then Java throws an IllegalThreadStateException. For example, this happens when you call suspend on a thread that is not currently runnable.

Dead Threads

A thread is dead for one of two reasons:

  • It dies a natural death because the run method exits.

  • It is killed because someone invoked its stop method.

If you stop a thread, it does not immediately die. Technically, the stop method throws an object of type ThreadDeath to the thread object. Once the thread passes a ThreadDeath object to its Thread base class, the thread truly dies. ThreadDeath is a child class of Error, not Exception, so you do not have to catch it. In most circumstances, you would not want to catch this. The exceptions are in the rare situations when you must do some clean-up prior to the thread being permanently put to rest.

NOTE

NOTE

If you do catch ThreadDeath, be sure to rethrow it in order to make the thread actually die.

Finding Out the State of a Thread

To find out whether a thread is currently runnable or blocked, use the isAlive method. This method returns false if the thread is still new and not yet runnable or if the thread is dead.

NOTE

NOTE

You cannot find out if a thread is actually running, nor can you differentiate between a thread that has not yet become runnable and one that has already died.

Thread Priorities

Every thread in Java has a priority. By default, a thread inherits the priority of its parent thread. You can increase or decrease the priority of any thread with the setPriority method. You can set the priority to any value between MIN_PRIORITY (defined as 1 in the Thread class) and MAX_PRIORTY (defined as 10). NORM_PRIORITY is defined as 5.

Whenever the thread-scheduler has a chance to pick a new thread, it picks the highest priority thread that is currently runnable. The highest-priority runnable thread keeps running until:

  • It yields by calling the yield method, or

  • It ceases to be runnable (either by dying or by entering the blocked state), or

  • It is replaced by a higher-priority thread that has become runnable (because it has slept long enough, because its I/O operation is complete, or because someone called resume or notify ).

Then, the scheduler selects a new thread to run. The highest-priority remaining thread is picked among those that are runnable.

What happens if there is more than one thread with the same priority? Each thread at that priority gets a turn in round-robin fashion. In other words, a thread is not scheduled again for execution until all other threads with the same priority have been scheduled at least once.

Consider the following test program, which modifies the previous program to run the threads of one kind of balls (displayed in red) with a higher priority than the other threads. (Set by the line in bold below.)

If you click on the Start button, five threads are launched at the normal priority, animating five black balls. If you click on the Express button, then you launch five red balls whose thread runs at a higher priority than the regular balls.

class BounceExpress 
{  public BounceExpress() 
   {  Panel p = new Panel(); 
      addButton(p, "Start", 
         new ActionListener() 
         {  public void actionPerformed(ActionEvent evt) 
            {  for (int i = 0; i < 5; i++) 
               {  Ball b = new Ball(canvas, Color.black); 
                  b.setPriority(Thread.NORM_PRIORITY); 
                  b.start(); 
               } 
            } 
         }); 
      addButton(p, "Express", 
         new ActionListener() 
         {  public void actionPerformed(ActionEvent evt) 
            {  for (int i = 0; i < 5; i++) 
               {  Ball b = new Ball(canvas, Color.red); 
                  b.setPriority(Thread.NORM_PRIORITY + 2); 
                  b.start(); 
               } 
            } 
         }); 
      . . . 
   } 
}

Try it out. Launch a set of regular balls and a set of express balls. You will notice that the express balls seem to run faster. This is solely a result of their higher priority, not because the red balls run at a higher speed. The code to move the express balls is the same as that of the regular balls.

Here is why this demonstration works: 5 milliseconds after an express thread goes to sleep, the scheduler wakes it. Now:

  • The scheduler again evaluates the priorities of all the runnable threads.

  • It finds that the express threads have the highest priority.

One of the express balls gets another turn right away. This can be the one that just woke up, or perhaps it is another express thread—you have no way of knowing. The express threads take turns, and only when they are all asleep does the scheduler give the lower-priority threads a chance to run. See Figure 2-6 and Example 2-3.

Threads with different priorities

Figure 2-6. Threads with different priorities

Example 2-3. BounceExpress.java

import java.awt.*; 
import java.awt.event.*; 
import corejava.*; 

public class BounceExpress extends CloseableFrame 
{  public BounceExpress() 
   {  canvas = new Canvas(); 
      add(canvas, "Center"); 
      Panel p = new Panel(); 
      addButton(p, "Start", 
         new ActionListener() 
         {  public void actionPerformed(ActionEvent evt) 
            {  for (int i = 0; i < 5; i++) 
               {  Ball b = new Ball(canvas, Color.black); 
                  b.setPriority(Thread.NORM_PRIORITY); 
                  b.start(); 
               } 
            } 
         }); 

      addButton(p, "Express", 
         new ActionListener() 
         {  public void actionPerformed(ActionEvent evt) 
            {  for (int i = 0; i < 5; i++) 
               {  Ball b = new Ball(canvas, Color.red); 
                  b.setPriority(Thread.NORM_PRIORITY + 2); 
                  b.start(); 
               } 
            } 
         }); 

      addButton(p, "Close", 
         new ActionListener() 
         {  public void actionPerformed(ActionEvent evt) 
            {  canvas.setVisible(false); 
               System.exit(0); 
            } 
         }); 
      add(p, "South"); 
   } 

   public void addButton(Container c, String title, 
      ActionListener a) 
   {  Button b = new Button(title); 
      c.add(b); 
      b.addActionListener(a); 
   } 
   public static void main(String[] args) 
   {  Frame f = new BounceExpress(); 
      f.show(); 
   } 

   private Canvas canvas; 
} 

class Ball extends Thread 
{  public Ball(Canvas c, Color co) { box = c; color = co; } 

   public void draw() 
   {  Graphics g = box.getGraphics(); 
      g.setColor(color); 
      g.fillOval(x, y, XSIZE, YSIZE); 
      g.dispose(); 
   } 

   public void move() 
   {  if (!box.isVisible()) return; 
      Graphics g = box.getGraphics(); 
      g.setColor(color); 
      g.setXORMode(box.getBackground()); 
      g.fillOval(x, y, XSIZE, YSIZE); 
      x += dx; 
      y += dy; 
      Dimension d = box.getSize(); 
      if (x < 0) 
      { x = 0; dx = -dx; } 
      if (x + XSIZE >= d.width) 
      { x = d.width - XSIZE; dx = -dx; } 
      if (y < 0) 
      { y = 0; dy = -dy; } 
      if (y + YSIZE >= d.height) 
      { y = d.height - YSIZE; dy = -dy; } 
      g.fillOval(x, y, XSIZE, YSIZE); 
      g.dispose(); 
   } 

   public void run() 
   {  draw(); 
      for (int i = 1; i <= 1000; i++) 
      {  move(); 
         try { Thread.sleep(5); } 
         catch(InterruptedException e) {} 
      } 
   } 
   private Canvas box; 
   private static final int XSIZE = 10; 
   private static final int YSIZE = 10; 
   private int x = 0; 
   private int y = 0; 
   private int dx = 2; 
   private int dy = 2; 
   private Color color; 
}

Cooperating and Selfish Threads

Our ball threads were well behaved and cooperated with each other. They did this by using the sleep function to wait their turns. The sleep function blocks the thread and gives the other threads a chance to be scheduled. Even if a thread does not want to put itself to sleep for any amount of time, it can call yield() whenever it does not mind being interrupted. A thread should always call yield or sleep when it is executing a long loop, to ensure that it is not monopolizing the system. A thread that does not follow this rule is called selfish.

The following program shows what happens when a thread contains a tight loop, a loop in which it carries out a lot of work without giving other threads a chance. When you click on the Selfish button, a blue ball is launched whose run method contains a tight loop.

class SelfishBall extends Ball 
{  . . . 
   public void run() 
   {  draw(); 
      for (int i = 1; i <= 1000; i++) 
      {  move(); 
         long t = System.currentTimeMillis(); 
         while (System.currentTimeMillis() < t + 5) 
            ; 
      } 
   } 
}

The run procedure will last about 5 seconds before it returns, ending the thread. In the meantime, it never calls yield or sleep.

What actually happens when you run this program depends on your operating system. For example, when you run this program under Solaris with the “green thread” package as opposed to the “native thread” package, you will find that the selfish ball indeed hogs the whole application. Try closing the program or launching another ball; you will have a hard time getting even a mouse-click into the application. However, when you run the same program under Windows 95 or NT, nothing untoward happens. The blue ball can run in parallel with other balls.

The reason is that the underlying thread package in Windows performs time-slicing. It periodically interrupts threads in midstream, even if they are not cooperating. When (even a selfish) thread is interrupted, Windows activates another thread—picked among the top-priority-level runnable threads. The green threads implementation used by Java 1.x on Solaris does not perform time-slicing, but the new native thread package does. If you know that your program will execute on a machine whose operating system performs time-slicing, then you do not need to worry about making your threads polite. But the point of Internet computing is that you generally do not know the environments of the people who will use your program. You should, therefore, plan for the worst and put calls to yield or sleep in every loop.

See Example 2-4 for the complete source code.

Example 2-4. BounceSelfish.java

import java.awt.*; 
import java.awt.event.*; 
import java.util.*; 
import corejava.*; 

public class BounceSelfish extends CloseableFrame 
{  public BounceSelfish() 
   {  canvas = new Canvas(); 
      add(canvas, "Center"); 
      Panel p = new Panel(); 
      addButton(p, "Start", 
         new ActionListener() 
         {  public void actionPerformed(ActionEvent evt) 
            {  Ball b = new Ball(canvas, Color.black); 
               b.setPriority(Thread.NORM_PRIORITY); 
               b.start(); 
            } 
         }); 

      addButton(p, "Express", 
         new ActionListener() 
         {  public void actionPerformed(ActionEvent evt) 
            {  Ball b = new Ball(canvas, Color.red); 
               b.setPriority(Thread.NORM_PRIORITY + 2); 
               b.start(); 
            } 
         }); 

      addButton(p, "Selfish", 
         new ActionListener() 
         {  public void actionPerformed(ActionEvent evt) 
            {  Ball b = new SelfishBall(canvas, Color.blue); 
               b.setPriority(Thread.NORM_PRIORITY + 2); 
               b.start(); 
            } 
         }); 

      addButton(p, "Close", 
         new ActionListener() 
         {  public void actionPerformed(ActionEvent evt) 
            {  canvas.setVisible(false); 
               System.exit(0); 
            } 
         }); 
      add(p, "South"); 
   } 
   public void addButton(Container c, String title, 
      ActionListener a) 
   {  Button b = new Button(title); 
      c.add(b); 
      b.addActionListener(a); 
   } 


   public static void main(String[] args) 
   {  Frame f = new BounceSelfish(); 
      f.show(); 
   } 

   private Canvas canvas; 
} 

class Ball extends Thread 
{  public Ball(Canvas c, Color co) { box = c; color = co; } 

   public void draw() 
   {  Graphics g = box.getGraphics(); 
      g.setColor(color); 
      g.fillOval(x, y, XSIZE, YSIZE); 
      g.dispose(); 
   } 

   public void move() 
   {  if (!box.isVisible()) return; 
      Graphics g = box.getGraphics(); 
      g.setColor(color); 
      g.setXORMode(box.getBackground()); 
      g.fillOval(x, y, XSIZE, YSIZE); 
      x += dx; 
      y += dy; 
      Dimension d = box.getSize(); 
      if (x < 0) 
      { x = 0; dx = -dx; } 
      if (x + XSIZE >= d.width) 
      { x = d.width - XSIZE; dx = -dx; } 
      if (y < 0) 
      { y = 0; dy = -dy; } 
      if (y + YSIZE >= d.height) 
      { y = d.height - YSIZE; dy = -dy; } 
      g.fillOval(x, y, XSIZE, YSIZE); 
      g.dispose(); 
   } 
   public void run() 
   {  draw(); 
      for (int i = 1; i <= 1000; i++) 
      {  move(); 
         try { Thread.sleep(5); } catch(InterruptedException e) {} 
      } 
   } 

   private Canvas box; 
   private static final int XSIZE = 10; 
   private static final int YSIZE = 10; 
   private int x = 0; 
   private int y = 0; 
   private int dx = 2; 
   private int dy = 2; 
   private Color color; 
} 

class SelfishBall extends Ball 
{  public SelfishBall(Canvas c, Color co) { super(c, co); } 

   public void run() 
   {  draw(); 
      for (int i = 1; i <= 1000; i++) 
      {  move(); 
         long t = System.currentTimeMillis(); 
         while (System.currentTimeMillis() < t + 5) 
            ; 
      } 
   } 
}

Thread Groups

Some programs contain quite a few threads. It then becomes useful to categorize them by functionality. For example, consider an Internet browser. If many threads are trying to acquire images from a server and the user clicks on a Stop button to interrupt the loading of the current page, then it is handy to have a way of killing all of these threads simultaneously. Java lets you construct what it calls a thread group in order to allow you to simultaneously work with a group of threads.

You construct a thread group with the constructor:

ThreadGroup g = new ThreadGroup(string)

The string identifies the group and must be unique. For example:

class ImageLoader extends ThreadGroup 
{  public ImageLoader(String name, ThreadGroup g) 
   {  super(g, "Loading " + name); 
      . . . 
   } 
}

To find out whether any threads of a particular group are still runnable, use the activeCount method.

if (g.activeCount() == 0) 
{  // all threads in the group g have stopped 
}

To kill all threads in a thread group, simply call stop on the group object.

g.stop(); // stops all threads in g

Thread groups can have child subgroups. By default, a newly created thread group becomes a child of the current thread group. But you can also explicitly name the parent group in the constructor (see the API notes). Methods such as activeCount and stop refer to all threads in their group and all child groups.

Synchronization

So far, we have not used threads that share the data segment. More precisely, a running thread can access any object to which it has a reference. Since it is possible for two threads to simultaneously have access to the same object, they can interfere with each other. For example, a thread might be trying to read a file that another thread is writing to. What happens if two threads have access to the same object and each calls a method that modifies the state of the object? As you might imagine, the threads step on each other’s toes, leading to corrupted objects. This is usually called the synchronization problem.

Thread Communication Without Synchronization

To solve the synchronization problem, you must learn how to synchronize access to shared objects. In this section, you’ll see what happens if you do not do this. In the next section, you’ll see how to synchronize object access in Java.

In the next test program, we simulate a bank with 10 accounts. We randomly generate transactions that move money between these accounts. There are 10 threads, one for each account. Each transaction moves a random amount of money from the account serviced by the thread to another random account.

The simulation code is straightforward. We have the class Bank with the method transfer. This method transfers some amount of money from one account to another. If the source account does not have enough money in it, then the thread calling the transfer method is put to sleep for a short time so that other threads have a chance to transfer some money into this account.

Here is the code for the transfer method of the Bank class.

public void transfer(int from, int to, int amount) 
{  while (accounts[from] < amount) 
   // wait for another thread to add more money 
   {  try {  Thread.sleep(5); } 
      catch(InterruptedException e) {} 
   } 

   accounts[from] -= amount; 
   accounts[to] += amount; 
   ntransacts++; 
   if (ntransacts % 5000 == 0) test(); 
}

Each thread generates a random transaction, calls transfer on the bank object, and then puts itself to sleep. Here is the code for the run method of the thread class.

public void run() 
{  while (true) 
   {  int to = (int)(Bank.NACCOUNTS  * Math.random()); 
      if (to == from) to = (to + 1) % Bank.NACCOUNTS; 
      int amount = (int)(Bank.INITIAL_BALANCE * Math.random()); 
      bank.transfer(from, to, amount); 
      try { sleep(1); } catch(InterruptedException e) {} 
   } 
}

When this simulation runs, we do not know how much money is in any one bank account at any time. But we do know that the total amount of money in all the accounts should remain unchanged since all we do is move money from one account to another.

Every 5,000 transactions, the transfer method calls a test method that recomputes the total and prints it out.

This program never finishes. Just press CTRL+C to kill the program.

Here is a typical printout:

Transactions:0 Sum: 100000 
Transactions:5000 Sum: 100000 
Transactions:10000 Sum: 100000 
Transactions:15000 Sum: 99840 
Transactions:20000 Sum: 98079 
Transactions:25000 Sum: 98079 
Transactions:30000 Sum: 98079 
Transactions:35000 Sum: 98079 
Transactions:40000 Sum: 98079 
Transactions:45000 Sum: 98079 
Transactions:50000 Sum: 98079 
Transactions:55000 Sum: 98079 
Transactions:60000 Sum: 98079 
Transactions:65000 Sum: 95925 
Transactions:70000 Sum: 95925 
Transactions:75000 Sum: 95925

As you can see, something is very wrong. For several thousand transactions, the bank balance remains at $100,000, which is the correct total for 10 accounts of $10,000 each. But after some time, the balance changes slightly. This situation does not inspire confidence, and we would probably not want to deposit our hard-earned money into this bank.

Example 2-5 provides the complete source code. See if you can spot the problem with the code. We will unravel the mystery in the next section.

Example 2-5. UnsynchBankTest.java

class UnsynchBankTest 
{  public static void main(String[] args) 
   {  Bank b = new Bank(); 
      int i; 
      for (i = 1; i <= Bank.NACCOUNTS; i++) 
         new TransactionSource(b, i).start(); 
   } 
} 

class Bank 
{  public Bank() 
   {  accounts = new long[NACCOUNTS]; 
      int i; 
      for (i = 0; i < NACCOUNTS; i++) 
         accounts[i] = INITIAL_BALANCE; 
      ntransacts = 0; 
      test(); 
   } 
   public void transfer(int from, int to, int amount) 
   {  while (accounts[from] < amount) 
      {  try {  Thread.sleep(5); } 
         catch(InterruptedException e) {} 
      } 

      accounts[from] -= amount; 
      accounts[to] += amount; 
      ntransacts++; 
      if (ntransacts % 5000 == 0) test(); 
   } 

   public void test() 
   {  int i; 
      long sum = 0; 

      for (i = 0; i < NACCOUNTS; i++) sum += accounts[i]; 
      System.out.println("Transactions:" + ntransacts 
         + " Sum: " + sum); 
   } 

   public static final int INITIAL_BALANCE = 10000; 
   public static final int NACCOUNTS = 10; 
   private long[] accounts; 
   private int ntransacts; 
} 

class TransactionSource extends Thread 
{  public TransactionSource(Bank b, int i) 
   {  from = i - 1; 
      bank = b; 
   } 

   public void run() 
   {  while (true) 
      {  int to = (int)(Bank.NACCOUNTS * Math.random()); 
         if (to == from) to = (to + 1) % Bank.NACCOUNTS; 
         int amount = (int)(Bank.INITIAL_BALANCE 
            * Math.random()); 
         bank.transfer(from, to, amount); 
         try { sleep(1); } catch(InterruptedException e) {} 
      } 
   } 

   private Bank bank; 
   private int from; 
}

Synchronizing Access to Shared Resources

In the previous section, we ran a program in which several threads updated bank account balances. After a few thousand transactions, errors crept in and some amount of money was either lost or spontaneously created. This problem occurs when two threads are simultaneously trying to update an account. Suppose two threads simultaneously carry out the instruction:

accounts[to] += amount;

The problem is that these are not atomic operations. The instruction might be processed as follows:

  1. Load accounts[to] into a register.

  2. Add amount.

  3. Move the result back to accounts[to].

Now, suppose the first thread executes Steps 1 and 2, and then it is interrupted. Suppose the second thread awakens and updates the same entry in the account array. Then, the first thread awakens and completes its Step 3.

That action wipes out the modification of the other thread. As a result, the total is no longer correct. (See Figure 2-7.)

Simultaneous access by two threads

Figure 2-7. Simultaneous access by two threads

Our test program detects this corruption. (Of course, there is a slight chance of false alarms if the thread is interrupted as it is performing the tests!)

NOTE

NOTE

You can actually peek at the Java bytecodes that execute each statement in our class. Run the command

javap -c -v Bank

to decompile the Bank.class file. (You need to unzip javalibclasses.zip for this to work.) For example, the line

accounts[to] += amount;

is translated into the following bytecodes.

aload_0 
getfield #16 <Field Bank.accounts [J> 
iload_1 
dup2 
laload 
iload_3 
i2l 
lsub 
lastore

What these codes mean does not matter. The point is that the increment command is made up of several instructions, and the thread executing them can be interrupted at the point of any instruction.

What is the chance of this happening? It is fairly common when the operating system performs time-slicing on threads. Our test program shows a corruption of the buffer every few thousand entries under Windows 95. When a thread system, such as the green threads on Solaris, does not perform time-slicing on threads, corruption is less frequent. (To simulate the behavior on Solaris with green threads, run another higher-priority thread that occasionally wakes up. When it goes to sleep, another transaction thread is scheduled.)

Of course, as Java programmers, we must cope with the worst possible scenario and must assume that threads will be frequently interrupted.

We must have some way of ensuring that once a thread has begun inserting an element into the buffer, it can complete the operation without being interrupted. Most programming environments force the programmer to fuss with so-called semaphores and critical sections to gain uninterrupted access to a resource—the fiddling is painful and the process is error prone. Java has a nicer mechanism, inspired by the monitors invented by Tony Hoare.

You simply tag any operation shared by multiple threads as synchronized, like this:

public synchronized void transfer(int from, int to, 
   int amount) 
{  . . . 
   accounts[from] -= amount; 
   accounts[to] += amount; 
   ntransacts++; 
   if (ntransacts % 5000 == 0) test(); 
   . . . 
}

When one thread calls a synchronized method, Java guarantees that the method will finish before another thread can execute any synchronized method on the same object. When one thread calls transfer and then another thread also calls transfer, the second thread cannot continue. Instead, it is deactivated and must wait for the first thread to finish executing the transfer method.

When you create an object with one or more synchronized methods, Java sets up a queue of all the threads waiting to be “let inside” the object. This is shown in Figure 2-8. Whenever one thread has completed its work with the object, the highest-priority thread in the waiting queue gets the next turn.

A monitor

Figure 2-8. A monitor

An object that can block threads and notify them when it becomes available is called a monitor. In Java, any object with one or more synchronized methods is a monitor.

In our simulation of the bank, we do not want to transfer money out of an account that does not have the funds to cover it. Note that we cannot use code like:

if (bank.getBalance(from) >= amount) 
   bank.transfer(from, to, amount);

It is entirely possible that the current thread will be interrupted between the successful outcome of the test and the call to transfer. By the time the thread is running again, the account balance may have fallen below amount. We must make sure that the thread cannot be interrupted between the test and the insertion. You ensure this by putting the test inside a synchronized version of the transfer method:

public synchronized void transfer(int from, int to, 
   int amount) 
{  while (accounts[from] < amount) 
   {  // wait 
   } 
   // transfer funds 
}

Now, what do we do when there is not enough money in the account? We wait until some other thread has added funds. But this thread has just gained exclusive access to the bank object, so no other thread has a chance to make a deposit. A second feature of synchronized methods takes care of this situation. You use the wait method in the thread class.

When a thread calls wait inside a synchronized method, that thread is deactivated, and Java puts it in the waiting queue for that object. This lets in another thread that can, hopefully, change the account balance. Java awakens the original threads again when another method calls the notifyAll method. This is the signal that the state of the bank object has changed and that waiting threads should be given another chance to inspect the object state. In our buffer example, we will call notifyAll when we have finished with the funds transfer.

This notification gives the waiting threads the chance to run again. A thread that was waiting for a higher balance then gets a chance to check the balance again. If the balance is sufficient, the thread performs the transfer. If not, it calls wait again.

It is important that the notifyAll function is called by some thread—otherwise, the threads that called wait will wait forever. The waiting threads are not automatically reactivated when no other thread is working on the object.

TIP

If your multithreaded program gets stuck, double-check that every wait is matched by a notifyAll. There is also a method notify that is less useful. It randomly awakens one thread, and if that thread can’t proceed, then the system can deadlock. Unless you are certain that all waiting threads can proceed, you should use notifyAll, not notify.

Here is the code for the transfer method that uses synchronized methods. Notice the calls to wait and notifyAll.

public synchronized void transfer(int from, int to, int 
   amount) 
{  while (accounts[from] < amount) 
   {  try {wait();} catch(InterruptedException e) {} 
   } 
   accounts[from] -= amount; 
   accounts[to] += amount; 
   ntransacts++; 
   if (ntransacts % 5000 == 0) test(); 
   notifyAll(); 
}

If you run the sample program with the synchronized version of the transfer method, you will notice that nothing ever goes wrong. The total balance stays at $100,000 forever. (Again, you need to press CTRL+C to terminate the program.)

You will also notice that the program in Example 2-6 runs a bit slower—this is the price you pay for the added bookkeeping involved in the synchronization mechanism.

Example 2-6. SynchBankTest.java

class SynchBankTest 
{  public static void main(String[] args) 
   {  Bank b = new Bank(); 
      int i; 
      for (i = 1; i <= Bank.NACCOUNTS; i++) 
         new TransactionSource(b, i).start(); 
   } 
} 

class Bank 
{  public Bank() 
   {  accounts = new long[NACCOUNTS]; 
      int i; 
      for (i = 0; i < NACCOUNTS; i++) 
         accounts[i] = INITIAL_BALANCE; 
      ntransacts = 0; 
      test(); 
   } 

   public synchronized void transfer(int from, int to, 
      int amount) 
   {  while (accounts[from] < amount) 
      {  try { wait(); } catch(InterruptedException e) {} 
      } 
      accounts[from] -= amount; 
      accounts[to] += amount; 
      ntransacts++; 
      if (ntransacts % 5000 == 0) test(); 
      notifyAll(); 
   } 

   public void test() 
   {  int i; 
      long sum = 0; 

      for (i = 0; i < NACCOUNTS; i++) sum += accounts[i]; 
      System.out.println("Transactions:" + ntransacts 
         + " Sum: " + sum); 
   } 

   public static final int INITIAL_BALANCE = 10000; 
   public static final int NACCOUNTS = 10; 

   private long[] accounts; 
   private int ntransacts; 
} 

class TransactionSource extends Thread 
{  public TransactionSource(Bank b, int i) 
   {  from = i - 1; 
      bank = b; 
   } 

   public void run() 
   {  while (true) 
      {  int to = (int)(Bank.NACCOUNTS * Math.random()); 
         if (to == from) to = (to + 1) % Bank.NACCOUNTS; 
         int amount = (int)(Bank.INITIAL_BALANCE 
            * Math.random()); 
         bank.transfer(from, to, amount); 
         try { sleep(1); } catch(InterruptedException e) {} 
      } 
   } 

   private Bank bank; 
   private int from; 
}

Here is a summary of how the synchronization mechanism works.

  1. If a class has one or more synchronized methods, each object of the class gets a queue that holds all threads waiting to execute one of the synchronized methods.

  2. There are two ways for a thread to get onto this queue: either by calling the method while another thread is using the object or by calling wait while using the object.

  3. When a synchronized method call returns or when a method calls wait, another thread gets access to the object.

  4. As always, the scheduler chooses the highest-priority thread among those in the queue.

  5. If a thread was put in the queue by a call to wait, it must be “unfrozen” by a call to notifyAll or notify before it can be scheduled for execution again.

The scheduling rules are undeniably complex, but it is actually quite simple to put them into practice. Just follow these three rules:

  1. If two or more threads modify an object, declare the methods that carry out the modifications as synchronized.

  2. If a thread must wait for the state of an object to change, it should wait inside the object, not outside, by entering a synchronized method and calling wait.

  3. Whenever a method changes the state of an object, it should call notifyAll. That gives the waiting threads a chance to see if circumstances have changed.

Deadlocks

The synchronization feature in Java is convenient and powerful, but it cannot solve all problems that might arise in multithreading. Consider the following situation:

  • Account 1: $2,000

  • Account 2: $3,000

  • Thread 1: Transfer $3,000 from Account 1 to Account 2

  • Thread 2: Transfer $4,000 from Account 2 to Account 1

As Figure 2-9 indicates, Threads 1 and 2 are clearly blocked. Neither can proceed since the balances in Accounts 1 and 2 are insufficient.

A deadlock situation

Figure 2-9. A deadlock situation

Is it possible that all 10 threads are blocked because each is waiting for more money? Such a situation is called a deadlock.

In our program, a deadlock could not occur for a simple reason. Each transfer amount is for, at most, $10,000. Since there are 10 accounts and a total of $100,000 in them, at least one of the accounts must have more than $10,000 at any time. The thread moving money out of that account can, therefore, proceed.

But if you change the run method of the threads to remove the $10,000 transaction limit, deadlocks can occur quickly. (Another way to create a deadlock is to make the i ’th thread responsible for putting money into the i ’th account, rather than for taking it out of the i ’th account. In this case, there is a small chance that all threads will gang up on one account, each trying to remove more money from it than it contains.)

Here is another situation in which a deadlock can occur easily: Change the notifyAll method to notify in the SynchBankTest program. You will find that the program hangs quickly. Unlike notifyAll, which notifies all threads that are waiting for added funds, the notify method activates only one thread. If that thread can’t proceed, all threads can be blocked. Consider the following sample scenario of a developing deadlock.

  • Account 1: $19,000

  • All other accounts: $9,000 each

  • Thread 1: Transfer $9,500 to from Account 1 to Account 2

  • All other threads: Transfer $9,100 from their account to another account

Clearly, all threads but Thread 1 are blocked since there isn’t enough money in their accounts.

Thread 1 proceeds. Afterward, we have the following situation:

  • Account 1: $9,500

  • Account 2: $18,500

  • All other accounts: $9,000 each

Then, Thread 1 calls notify. The notify method picks a thread at random. Suppose it picks Thread 3. That thread is awakened, finds that there isn’t enough money in its account, and calls wait again. But Thread 1 is still running.

A new random transaction is generated, say,

  • Thread 1: Transfer $9,600 to from Account 1 to Account 2

Now, Thread 1 also calls wait, and all threads are blocked. The system has deadlocked.

There is nothing that Java can do to avoid or break these deadlocks. You must design your threads to ensure that a deadlock situation cannot occur. Analyze your program and ensure that every thread awaiting an object will eventually be activated. For example, in the preceding scenario, calling notifyAll instead of notify avoids the deadlock.

Thread synchronization and deadlock avoidance are difficult subjects, and we refer the interested reader to the book Programming with Threadsby Steve Kleiman, Devang Shah, and Bart Smaalders [Sunsoft Press/Prentice-Hall, 1996].

Using Pipes for Communication between Threads

Sometimes, the communication pattern between threads is very simple. One thread, the so-called producer, generates a stream of bytes. Another thread, the so-called consumer, reads and processes that byte stream. If no bytes are available for reading, the consumer thread blocks. If the producer generates data much more quickly than the consumer can handle it, then the write operation of the producer thread blocks. Java has a convenient set of classes, PipedInputStream and PipedOutputStream, to implement this communication pattern. (There is another pair of classes, PipedReader and PipedWriter, if the producer thread generates a stream of Unicode characters instead of bytes.)

The principal reason to use pipes is to keep each thread simple. The producer thread simply sends its results to a stream and forgets about them. The consumer simply reads the data from a stream, without having to care where it comes from. By using pipes, multiple threads can be connected with each other.

Example 2-7 is a program that shows off piped streams. We have a producer thread that emits random numbers at random times, a filter thread that reads the input numbers and continuously computes the average of the data, and a consumer thread that prints out the answers. (You’ll need to use Ctrl+C to stop this program.) Figure 2-10 shows the threads and the pipes that connect them. Unix users will recognize these pipe streams as the equivalent of pipes connecting processes in Unix.

A sequence of pipes

Figure 2-10. A sequence of pipes

Example 2-7. PipeTest.java

import java.util.*; 
import java.io.*; 

public class PipeTest 
{  public static void main(String args[]) 
   {  try 
      {  /* set up pipes */ 
         PipedOutputStream pout1 = new PipedOutputStream(); 
         PipedInputStream pin1 = new PipedInputStream(pout1); 

         PipedOutputStream pout2 = new PipedOutputStream(); 
         PipedInputStream pin2 = new PipedInputStream(pout2); 

         /* construct threads */ 

         Producer prod = new Producer(pout1); 
         Filter filt = new Filter(pin1, pout2); 
         Consumer cons = new Consumer(pin2); 

         /* start threads */ 

         prod.start(); 
         filt.start(); 
         cons.start(); 
      } 
      catch (IOException e){} 
   } 
} 

class Producer extends Thread 
{  public Producer(OutputStream os) 
   {  out = new DataOutputStream(os); 
   } 

   public void run() 
   {  for(;;) 
      {  try 
         {  double num = rand.nextDouble(); 
            out.writeDouble(num); 
            out.flush(); 
            sleep(Math.abs(rand.nextInt() % 1000)); 
         } 
         catch(Exception e) 
         {  System.out.println("Error: " + e); 
         } 
      } 
   } 
   private DataOutputStream out; 
   private Random rand = new Random(); 
} 

class Filter extends Thread 
{  public Filter(InputStream is, OutputStream os) 
   {  in = new DataInputStream(is); 
      out = new DataOutputStream(os); 
   } 

   public void run() 
   {  for (;;) 
      {  try 
         {  double x = in.readDouble(); 
            total += x; 
            count++; 
            if (count != 0) out.writeDouble(total / count); 
         } 
         catch(IOException e) 
         {  System.out.println("Error: " + e); 
         } 
      } 
   } 

   private DataInputStream in; 
   private DataOutputStream out; 
   private double total = 0; 
   private int count = 0; 
} 

class Consumer extends Thread 
{  public Consumer(InputStream is) 
   {   in = new DataInputStream(is); 
   } 

   public void run() 
   {  for(;;) 
      {  try 
         {  double avg = in.readDouble(); 
            if (Math.abs(avg - old_avg) > 0.01) 
            {  System.out.println("Current average is " + avg); 
               old_avg = avg; 
            } 
         } 
         catch(IOException e) 
         {  System.out.println("Error: " + e); 
         } 
      } 
   } 
   private double old_avg = 0; 
   private DataInputStream in; 
}

Timers

In many programming environments, you can set up timers. A timer alerts your program elements at regular intervals. For example, to display a clock in a window, the clock object must be notified once every second.

Java does not have a built-in timer class, but it is easy enough to build one using threads. In this section, we describe how to do that. You can use this timer class in your own code.

Our timer runs in its own thread, so it must extend Thread. In its run method, it goes to sleep for the specified interval, then notifies its target.

class Timer extends Thread 
{  . . . 
  public void run() 
   {  while (true) 
      {  try { sleep(interval); } 
           catch(InterruptedException e) {} 
         // notify target 
      } 
   } 

   private int interval; 
}

There is a slight problem with writing a general-purpose timer class. The timer holds an object reference to the target, and it is supposed to notify the object whenever the time interval has elapsed. In C or C++ programming, the timer object would hold a pointer to a function, and it would call that function periodically. For safety reasons, Java does not make it easy to use method pointers. (As we have seen in Chapter 5 of Volume 1, the reflection mechanism provides method pointers, but they are slow and tedious to use.)

We showed you in Chapter 5 of Volume 1 how to overcome this problem by making a special interface for the callback. In our case, the interface is called Timed and the callback is called tick. Thus, an object that wants to receive timer ticks must implement the Timed interface. The action to be repeated in regular intervals must be put into the tick method.

Figure 2-11 shows six different clocks.

Clock threads

Figure 2-11. Clock threads

Each clock is an instance of the ClockCanvas class, which implements Timed. The tick method of the Clock class redraws the clock.

class ClockCanvas extends Canvas implements Timed 
{  . . . 
   public void tick(Timer t) 
   {  GregorianCalendar d = new GregorianCalendar(); 
      seconds = (d.get(Calendar.HOUR) - LOCAL + offset) 
         * 60 * 60 + d.get(Calendar.MINUTE) * 60 
         + d.get(Calendar.SECOND); 
      repaint(); 
   } 
}

Note that the tick method does not actually get the time from the timer object t. The timer ticks come only approximately once a second. For an accurate clock display, we still need to get the system time.

Example 2-8 is the complete code.

Example 2-8. TimerTest.java

import java.awt.*; 
import java.util.*; 
import corejava.*; 

public class TimerTest extends CloseableFrame 
{  public TimerTest() 
   {  setTitle("TimerTest"); 
      setLayout(new GridLayout(2, 3)); 
      add(new ClockCanvas("San Jose", 16)); 
      add(new ClockCanvas("Taipei", 8)); 
      add(new ClockCanvas("Berlin", 1)); 
      add(new ClockCanvas("New York", 19)); 
      add(new ClockCanvas("Cairo", 2)); 
      add(new ClockCanvas("Bombay", 5)); 
   } 

   public static void main(String[] args) 
   {  Frame f = new TimerTest(); 
      f.setSize(450, 300); 
      f.show(); 
   } 
} 

interface Timed 
{  public void tick(Timer t); 
} 

class Timer extends Thread 
{  public Timer(Timed t, int i) 
   {  target  = t; interval = i; 
      setDaemon(true); 
   } 

   public void run() 
   {  while (true) 
      {  try { sleep(interval); } 
         catch(InterruptedException e) {} 
         target.tick(this); 
      } 
   } 

   private Timed target; 
   private int interval; 
} 
class ClockCanvas extends Canvas implements Timed 
{  public ClockCanvas(String c, int off) 
   {  city = c; offset = off; 
      new Timer(this, 1000).start(); 
      setSize(125, 125); 
   } 

   public void paint(Graphics g) 
   {  g.drawOval(0, 0, 100, 100); 
      double hourAngle = 2 * Math.PI 
         * (seconds - 3 * 60 * 60) / (12 * 60 * 60); 
      double minuteAngle = 2 * Math.PI 
         * (seconds - 15 * 60) / (60 * 60); 
      double secondAngle = 2 * Math.PI 
         * (seconds - 15) / 60; 
      g.drawLine(50, 50, 50 + (int)(30 
         * Math.cos(hourAngle)), 
         50 + (int)(30 * Math.sin(hourAngle))); 
      g.drawLine(50, 50, 50 + (int)(40 
         * Math.cos(minuteAngle)), 
         50 + (int)(40 * Math.sin(minuteAngle))); 
      g.drawLine(50, 50, 50 + (int)(45 
         * Math.cos(secondAngle)), 
         50 + (int)(45 * Math.sin(secondAngle))); 
      g.drawString(city, 0, 115); 
   } 

   public void tick(Timer t) 
   {  GregorianCalendar d = new GregorianCalendar(); 
      seconds = (d.get(Calendar.HOUR) - LOCAL + offset) 
         * 60 * 60 + d.get(Calendar.MINUTE) * 60 
         + d.get(Calendar.SECOND); 
      repaint(); 
   } 

   private int seconds = 0; 
   private String city; 
   private int offset; 
   private final int LOCAL = 16; 
}

Daemon Threads

If you look carefully into the constructor of the timer class above, you will note the method call that looks like this:

setDaemon(true);

This method call makes the timer thread a daemon thread. There is nothing demonic about it. A daemon is simply a thread that has no other role in life than to serve others. When only daemon threads remain, then the program exits. There is no point in keeping the program running if all remaining threads are daemons.

In a graphical application, the timer class threads do not affect when the program ends. It stays alive until the user closes the application. (The user-interface thread is not a daemon thread.) But when you use the timer class in a text application, you need not worry about stopping the timer threads. When the non-timer threads have finished their run methods, the application automatically terminates.

Animation

In the previous sections, you learned what is required to split a program into multiple concurrent tasks. Each task needs to be placed into a run method of a class that extends Thread. But what if we want to add the run method to a class that already extends another class? This occurs most often when we want to add multithreading to an applet. An applet class already inherits from Applet, and, in Java, we cannot inherit from two parent classes, so we need to use an interface. The necessary interface is built into Java. It is called Runnable. We take up this important interface next.

The Runnable Interface

Whenever you need to use multithreading in a class that is already derived from a class other than Thread, make the class implement the Runnable interface. As though you had derived from Thread, put the code that needs to run in the run method. For example,

class Animation extends Applet implements Runnable 
{  . . . 
   public void run() 
   {  // thread action goes here 
   } 
}

You still need to make a thread object to launch the thread. Give that thread a reference to the Runnable object in its constructor. The thread then calls the run method of that object.

This call is most commonly made in the start method of an applet, as in the following example:

class Animation extends Applet implements Runnable 
{  . . . 
   public void start() 
   {  if (runner == null) 
      {  runner = new Thread(this); 
         runner.start(); 
      } 
   } 
   . . . 
   private Thread runner; 
}

In this case, the this argument to the Thread constructor specifies that the object whose run function should be called when the thread executes is an instance of the Animation object.

Wouldn’t it be easier if we just defined another class from Thread and launched it in the applet?

class AnimationThread extends Thread 
{   public void run() 
   {  // thread action goes here 
   } 
} 

class Animation extends Applet 
{  . . . 
   public void start() 
   {  if (runner == null) 
      {  runner = new AnimationThread(); 
         runner.start(); 
      } 
   } 
   . . . 
   private Thread runner; 
}

Indeed, this would be clean and simple. However, if the run method must have access to an applet’s private data, then it makes sense to keep the run method with the applet and use the Runnable interface instead.

Loading and Displaying Frames

In this section, we dissect one of the most common uses for threads in Java applets: animation. An animation sequence displays images, giving the viewer the illusion of motion. Each of the images in the sequence is called a frame. Of course, the frames must be rendered ahead of time—today’s personal computers do not have the horsepower to compute the drawing fast enough for real-time animation.

You can put each frame in a separate file or put all frames into one file. We do the latter. It makes the process of loading the image much easier. In our example, we use a file with 36 images of a rotating globe. Figure 2-12, courtesy of Silviu Marghescu of the University of Maryland, shows the first few frames.

This file has 36 images of a globe

Figure 2-12. This file has 36 images of a globe

The animation applet must first acquire all the frames. Then, it shows each of them, in turn, for a fixed time. You know from Chapter 6 of Volume 1 how to load an image file. When you call getImage, the image is not actually loaded. Instead, the first time you access the image data with prepareImage, a separate thread is spawned to start the loading process. Loading an image can be very slow, especially if it has many frames or is located across the network. The thread acquiring the image periodically calls the imageUpdate method of the applet. Eventually, the ALLBITS flag of the infoflags parameter is set, and the image is complete.

Once the image is loaded, we render one frame at a time. To draw the i ’th frame, we make a method call as follows:

g.drawImage(image, 0, - i * imageHeight / imageCount, null);

Figure 2-13 shows the negative offset of the y-coordinate. This offset causes the first frame to be well above the origin of the canvas. The top of the i ’th frame becomes the top of the canvas.

Picking a frame from a strip of frames

Figure 2-13. Picking a frame from a strip of frames

After a delay, we increment i and draw the next frame.

Using a Thread to Control the Animation

Our applet will have a single thread.

class Animation implements Runnable 
{  . . . 
   Thread runner = null; 
}

You will see such a thread variable in many Java applets. Often, it is called kicker, and we once saw killer as the variable name. We think runner makes more sense, though.

First and foremost, we will use this thread to:

  • Start the animation when the user is watching the applet.

  • Stop the animation when the user has switched to a different page in the browser.

We do these tasks by creating the thread in the start method of the applet and by destroying it in the stop method. You can do this with the following code, which you will find in many applets.

class Animation implements Runnable 
{  public void start() 
   {  if (runner == null) 
      {  runner = new Thread(this); 
         runner.start(); 
      } 
   } 
   public void stop() 
   {  if (runner != null && runner.isAlive()) 
         runner.stop(); 
      runner = null; 
   } 

   . . . 
}

Note the method call:

runner = new Thread(this);

This call creates a thread that calls the run function of this applet.

Here is the run method. If the image is not yet loaded, the loadImage function is called to load it. Otherwise, the run function loops, painting the screen and sleeping when it can.

class Animation implements Runnable 
{  public void run() 
   {  loadImage(); 
      while (runner != null) 
      {  repaint(); 
         try { Thread.sleep(200); } 
         catch (InterruptedException e) {} 
     } 
   } 

   . . . 
}

The loadImage method of the Animation class (in the listing at the end of this section) is interesting. It is a synchronized method (that is, it is capable of blocking the current thread). When the thread enters the loadImage procedure, it creates the image object and then checks whether the image is already loaded. It does this by looking at the loaded instance variable of the Animation class. The first time around, that flag is certainly false, and the runner thread is suspended.

Whenever the image-loading thread has acquired another scan line of the image, it calls the imageUpdate method. (See Figure 2-14). Once the image is complete, it sets the loaded flag to true and calls notify() ! This awakens the runner thread that was pending in the loadImage method.

Threads in the image loading process

Figure 2-14. Threads in the image loading process

This description explains how the applet suppresses animation while image-loading is still in progress. If you do not implement this interlocking mechanism, partially loaded frames will be drawn, which looks quite unsightly.

TIP

If you load multiple images and audio files, monitoring their loading progress becomes tedious. The MediaTracker class does this for you automatically. In this example, we chose to do it by hand to show you what goes on behind the scenes.

Finally, we implement another method of stopping and restarting the animation. When you click with the mouse on the applet window, the animation stops. When you click again, it restarts. This behavior is implemented in the toggleAnimation method.

public class Animation extends Applet implements Runnable 
{  public void init() 
   {  addMouseListener(new MouseAdapter() 
         {  public void mouseClicked(MouseEvent evt) 
            {  toggleAnimation(); 
            } 
         }); 

      . . . 
   } 

   public void toggleAnimation() 
   {  if (loaded) 
      {  if (runner != null && runner.isAlive()) 
         {  if (stopped) 
            {  showStatus("Click to stop"); 
               runner.resume(); 
            } 
            else 
            {  showStatus("Click to restart"); 
               runner.suspend(); 
            } 
            stopped = !stopped; 
         } 
         else 
         {  stopped = false; 
            current = 0; 
            runner = new Thread(this); 
            runner.start(); 
         } 
      } 
   } 
   . . . 
}

Figure 2-15 shows the state-transition diagram of the runner thread.

State diagram for runner thread

Figure 2-15. State diagram for runner thread

The applet reads the name of the image and the number of frames in the strip from the PARAM section in the HTML file (on the CD-ROM as well).

<html> 
<title>Animation Applet</title> 
<body> 
<applet code=Animation.class width=100 height=100> 
<param name=imagename value="globe.gif"> 
<param name=imagecount value="36"> 
</applet> 
</body> 
</html>

Example 2-9 is the code of the applet. Have a close look at the interplay between the run, loadImage, paint, and imageUpdate methods.

Example 2-9. Animation.java

import java.awt.*; 
import java.awt.image.*; 
import java.awt.event.*; 
import java.applet.*; 
import java.net.*; 

public class Animation extends Applet implements Runnable 
{  public void start() 
   {  if (runner == null) 
      {  runner = new Thread(this); 
         runner.start(); 
      } 
   } 

   public void stop() 
   {  if (runner != null && runner.isAlive()) 
         runner.stop(); 
      runner = null; 
   } 

   public void init() 
   {  addMouseListener(new MouseAdapter() 
         {  public void mouseClicked(MouseEvent evt) 
            {  toggleAnimation(); 
            } 
         }); 

      try 
      {  imageName = getParameter("imagename"); 
         if (imageName == null) imageName = ""; 

         imageCount = 1; 
         String param = getParameter("imagecount"); 
         if (param != null) 
            imageCount = Integer.parseInt(param); 
      } 
      catch (Exception e) 
      {  showStatus("Error: " + e); 
      } 
   } 

   public synchronized void loadImage() 
   {  if (loaded) return; 
      try 
         {  URL url = new URL(getDocumentBase(), imageName); 
            showStatus("Loading " + imageName); 
            image = getImage(url); 
            prepareImage(image, this); 
         } 
         catch(Exception e) 
         {  showStatus("Error: " + e); 
         } 

      while (!loaded) 
         try { wait(); } catch (InterruptedException e) {} 
      resize(imageWidth, imageHeight / imageCount); 
   } 

   public void run() 
   {  loadImage(); 
      while (runner != null) 
      {  repaint(); 
         try { Thread.sleep(200); } 
         catch (InterruptedException e) {} 
         current = (current + 1) % imageCount; 
      } 
   } 

   public void paint(Graphics g) 
   {  if (!loaded) return; 

      g.drawImage(image, 0, - (imageHeight / imageCount) 
         * current, null); 
   } 

   public void update(Graphics g) 
   {  paint(g); 
   } 

   public void toggleAnimation() 
   {  if (loaded) 
      {  if (runner != null && runner.isAlive()) 
         {  if (stopped) 
            {  showStatus("Click to stop"); 
               runner.resume(); 
            } 
            else 
            {  showStatus("Click to restart"); 
               runner.suspend(); 
            } 
            stopped = !stopped; 
         } 
         else 
         {  stopped = false; 
            current = 0; 
            runner = new Thread(this); 
            runner.start(); 
         } 
      } 
   } 

   public synchronized boolean imageUpdate(Image img, 
      int infoflags, int x, int y, int width, int height) 
   {  if ((infoflags & ImageObserver.ALLBITS) != 0) 
      {  // image is complete 
         imageWidth = image.getWidth(null); 
         imageHeight = image.getHeight(null); 
         showStatus("Click to stop"); 
         loaded = true; 
         notify(); 
         return false; 
      } 
      return true; // want more info 
   } 

   private Image image; 
   private int imageCount; 
   private int imageWidth = 0; 
   private int imageHeight = 0; 
   private String imageName; 
   private Thread runner = null; 
   private int current = 0; 
   private boolean loaded = false; 
   private boolean stopped = false; 
}

This animation applet is simplified in order to show you what goes on behind the scenes. If you are interested only in how to put a moving image on your Web page, look instead at the Animator applet in the demo section of the JDK. That applet has many more options than ours, and it lets you add sound.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.15.85