Module 12: Threads


What is a thread?

A thread is a "lightweight" process, usually with a shared data segment.

If that doesn't make sense, consider:

About Java threads:


An example without threads

To see why threads are useful, let us first look at an example that does not use threads:

Here is the code: (source file)


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

class NewFrame extends Frame {

  Canvas c;

  public NewFrame ()
  {
    // Frame properties. 
    this.setTitle ("Dog Race");
    this.setBackground (Color.cyan);
    this.setResizable (true);
    this.setSize (600,200);

    // Quit button. 
    Panel p = new Panel ();
    Button quitb = new Button ("QUIT");
    quitb.addActionListener (
      new ActionListener () {
        public void actionPerformed (ActionEvent a)
        {
	  System.exit (0);
	}
      }
    );
    p.add (quitb);

    // Pressing "start" calls race() 
    Button startb = new Button ("START");
    startb.addActionListener (
      new ActionListener () {
        public void actionPerformed (ActionEvent a)
        {
	  race ();
	}
      }
    );
    p.add (startb);

    this.add (p, BorderLayout.SOUTH);

    // A canvas to draw the results. 
    c = new Canvas();
    c.setBackground (Color.white);
    this.add (c, BorderLayout.CENTER);

    this.setVisible (true);
  }


  void race ()
  {
    Dimension D = c.getSize ();

    // Finish-line is at the right end of the canvas. 
    int finish_line = D.width;

    // Create two dog instances with different ID's. 
    Dog d1 = new Dog (1, c);
    Dog d2 = new Dog (2, c);

    // Each dog sleeps a random amount of time. 
    int d1_nextwakeup = (int) UniformRandom.uniform (300,600);
    int d2_nextwakeup = (int) UniformRandom.uniform (300,600);

    // Keep track of the current time. 
    int current_time = 0;

    // Stop when one dog crosses the finish line. 

    while ( (d1.position < finish_line) && (d2.position < finish_line) ) {

      // See which one is done first. 
      if (d1_nextwakeup < d2_nextwakeup) {
	try {
	  // Static method "sleep" in class Thread. 
	  Thread.sleep (d1_nextwakeup - current_time);
	}
	catch (InterruptedException e) {
	  System.out.println (e);
	}
	current_time = d1_nextwakeup;
	d1.move(); 	// Move a random distance. 
	d1_nextwakeup += (int) UniformRandom.uniform (300,600);
      }
      else {
	try {
	  Thread.sleep (d2_nextwakeup - current_time);
	}
	catch (InterruptedException e) {
	  System.out.println (e);
	}
	current_time = d2_nextwakeup;
	d2.move();      // Move a random distance. 
	d2_nextwakeup += (int) UniformRandom.uniform (300,600);
      }
    }
  }

}


class Dog {

  public int position = 20;  // Starting position. 
  int ID;                    // An ID. 
  Canvas c;                  // The canvas on which to draw. 

  public Dog (int ID, Canvas c)
  {
    this.ID = ID;
    this.c = c;

    // Draw ID on canvas. 
    Graphics g = c.getGraphics ();
    g.drawString (""+ID, 5, 20*ID+8);
  }

  public void move ()
  {
    // Move a random amount. 
    int new_position = position + (int) UniformRandom.uniform (50,100);

    // Draw new position. 
    Graphics g = c.getGraphics ();
    int size = new_position - position;
    g.fillRect (position, 20*ID, size, 10);
    position = new_position;
  }
}

public class Race1 {
  public static void main (String[] argv)
  {
    NewFrame nf = new NewFrame ();
  }
}
  

Note:

Exercise 12.1 (Solution): Modify the above code to add a third dog to the race. You will also need to download UniformRandom.java.


The same example with threads

Now we will look at a thread-version of the above dog-race program:

  • Each dog will run as a separate thread.

  • Each such thread will sleep and move on its own.

  • To create and use the a thread, use the following steps:
    • Decide which class is going to run as a thread (or which class has the "main-loop" of the thread).
    • Make that class implement the Runnable interface.
    • To implement this interface, the class has to implement a method called public void run().
    • The run() method will be called when the thread is started.
    • Next, to actually run the thread, pass the selected class to a new instance of Thread and call the start() method of the Thread instance.

Here is the code using threads: (source file)


class NewFrame extends Frame {

  Canvas c;

  public NewFrame ()
  {
    // ... same as in Race1.java ... 
  }


  void race ()
  {
    Dimension D = c.getSize ();

    // Finish-line is at the right end of the canvas. 
    int finish_line = D.width;

    // Create two dog instances with different ID's. 
    Dog d1 = new Dog (1, c);
    Dog d2 = new Dog (2, c);

    // Create a Thread instance for each dog. 
    // Note: the class Dog must implement the 
    // Runnable interface. 
    Thread d1_thread = new Thread (d1);
    Thread d2_thread = new Thread (d2);

    // Start running the threads. 
    // ("start" is a method in Thread). 
    d1_thread.start();
    d2_thread.start();
  }

}


class Dog implements Runnable {

  // ... same as in Race1.java ... 

  public Dog (int ID, Canvas c)
  {
    // ... same as in Race1.java ... 
  }

  public void move ()
  {
    // ... same as in Race1.java ... 
  }

  // Must implement this method to implement 
  // the Runnable interface. 
  public void run ()
  {
    // Compute the finish line distance. 
    int finish_line = c.getSize().width;

    // While not complete... 
    while (position < finish_line) {
      // Sleep 
      try {
	Thread.sleep ((int)UniformRandom.uniform (300,600));
      }
      catch (InterruptedException e) {
	System.out.println (e);
      }

      // Move. 
      move ();
    }
  }

}


public class Race2 {
    // ... same as in Race1.java ... 
}
  

Note:

  • The class Dog now implements the Runnable interface.

  • The run() method of Dog contains the main loop for a single dog-thread: repeatedly sleep and move until the finish line is crossed.

  • Creating a thread is as simple as passing a Dog instance to the constructor of Thread:
    
        Thread d1_thread = new Thread (d1);
        
  • To actually start the thread going, one calls the start() method of the thread instance:
    
        d1_thread.start();
        
  • Once started, a thread continues running until the run() method completes, or the thread is blocked, or the thread is stopped (by the thread-creator).

  • An alternative to implementing Runnable is the following:
    • Have the target class (Dog) inherit from Thread and override run():
      
         class Dog extends Thread {
      
           //.. 
      
           // Override run() 
           public void run()
           {
             // ... 
           }
         }
      	
    • Then, simply call the inherited start() method to start the thread:
      
         Dog d1 = new Dog (1, c);
         d1.start();
              
    • However, if the target class already extend's something, you need to use the Runnable approach used earlier.
    • Using the Runnable approach seems cleaner.

Exercise 12.2 (Solution): Modify the above code to add a third dog to the race. You will also need to download UniformRandom.java. When you run your code, note the following:

  • Now you can "quit" the application during the race.
  • Notice what happens when one dog wins: the other dogs complete the race. Can you freeze the frame when first dog to win crosses the finish line?


Sharing data across threads

To solve the problem of "losers" continuing the race, we will have each thread read the status of the race via a method call:

  • We will define the method race_finished() in our frame, with the following signature:
    
      public boolean race_finished (boolean set)
      {
        // If set==true, set the race to be over. 
        // Otherwise, return current status. 
      }
        
  • Thus, the same method is called by each dog to either (1) indicate that the race is over; or (2) find out whether the race is over.

Here is the code: (source file)


class NewFrame extends Frame {

  // ... same as in Race2.java ... 

  void race ()
  {
    Dimension D = c.getSize ();

    // Finish-line is at the right end of the canvas. 
    int finish_line = D.width;

    // Create two dog instances with different ID's. 
    Dog d1 = new Dog (1, c, this);
    Dog d2 = new Dog (2, c, this);

    // Create a Thread instance for each dog. 
    // Note: the class Dog must implement the 
    // Runnable interface. 
    Thread d1_thread = new Thread (d1);
    Thread d2_thread = new Thread (d2);

    race_over = false;

    // Start running the threads. 
    // ("start" is a method in Thread). 
    d1_thread.start();
    d2_thread.start();
  }


  boolean race_over;

  // The same method is called to check whether the race 
  // is complete or to indicate that it is complete. 

  public boolean race_finished (boolean set)
  {
    boolean return_val = false;
    if (!set)
      return_val = race_over;
    else {
      race_over = true;
      return_val = race_over;
    }
    return return_val;
  }
}

class Dog implements Runnable {

  // ... 

  NewFrame f;  // To find out whether the race is over. 

  public Dog (int ID, Canvas c, NewFrame f)
  {
    this.ID = ID;
    this.c = c;

    // Draw ID on canvas. 
    Graphics g = c.getGraphics ();
    g.drawString (""+ID, 5, 20*ID+8);

    // Save the frame reference. 
    this.f = f;
  }

  public void move ()
  {
    // ... same as in Race2.java ... 
  }

  public void run ()
  {
    // Compute the finish line distance. 
    int finish_line = c.getSize().width;

    // While not complete... 
    while (position < finish_line) {
	try {
	  Thread.sleep ((int)UniformRandom.uniform (300,600));
	}
	catch (InterruptedException e) {
	  System.out.println (e);
	}

	// Check whether race is over. 
	if (f.race_finished (false)) break;

	// Move if race is still on. 
	move ();
    }
    if (position >= finish_line)
      f.race_finished (true);
  }

}

// ... 
  

Note:

  • Now each thread calls the race_finished() method in the frame to check the status.
  • A trial execution will show that this code solves the problem.
  • Or does it? There is a small subtlety we will look into next.


Synchronized access to shared data.

Let us demonstrate a problem with the above code by modifying the race_finished() method: (source file)


  public boolean race_finished (boolean set, int ID)
  {
    boolean return_val = race_over; 

    System.out.println ("finished: startofcode: ID=" + ID + ", set=" + set);
    if (!set) 
      return_val = race_over;
    else {
      // Race overseer sleeps for a while. 
      try {
	Thread.sleep ((int)UniformRandom.uniform (1000,2000));
      }
      catch (InterruptedException e) {
	System.out.println (e);
      }
      race_over = true;
      return_val = true;
    }

    System.out.println ("race_fin: endofcode: ID=" + ID + ", set=" + set);
    return return_val;
  }
  
Here's what we did:
  • If the caller is only checking the status, the code executes as before.

  • However, in the case that the race is actually over, the caller is put to sleep (for a longish time) in the else-part.

  • We have added the caller's ID to the parameter list of the method.

  • We print the caller's ID to screen when the code is entered and when the code completes.

When executed, you will notice that both dogs go across the finish line - thus, we have not solved the problem!

A look at the screen output shows why:


finished: startofcode: ID=2, set=true
finished: startofcode: ID=1, set=false
finished: endofcode: ID=1, set=false
finished: startofcode: ID=1, set=false
finished: endofcode: ID=1, set=false
finished: startofcode: ID=1, set=true
finished: endofcode: ID=2, set=true
finished: endofcode: ID=1, set=true
  
Note:
  • When Dog 2 gets into the method (first line above), it has actually finished the race, but it is put to sleep.

  • Then, Dog 1 also gets into the method and reads "incorrect" information.

  • The key is: both Dog threads should not be executing the method race_finished() concurrently.

The problem is easy to solve:

  • Simply include the modifier synchronized in the method race_finished(): (source file)
    
      public synchronized boolean race_finished (boolean set, int ID)
      {
    
         // ... everything else stays the same ... 
    
      }
        
Upon execution, the screen output shows that only one thread executes inside the body of race_finished():

finished: startofcode: ID=2, set=false
finished: endofcode: ID=2, set=false
finished: startofcode: ID=2, set=true
finished: endofcode: ID=2, set=true
finished: startofcode: ID=1, set=false
finished: endofcode: ID=1, set=false
  

Note:

  • The technique of making a method synchronized is one way of forcing one-at-a-time (mutually exclusive) access to data.

  • Any shared data that can be modified (such as a bank account) should be accessed only via synchronized methods.

  • Synchronized methods incur an overhead.

  • Sometimes, you only need to provide mutual exclusion for a small block of code:
    • In this case, you can force synchronized access to a block of code ("the critical section") as follows:
      
           synchronized (SomeSynchObject) {
             // Critical section 
           }
            
    • However, it is just as easy to place the block of code in a method and synchronize the method.


Using static variables to share data.

The above solution of calling race_finished() in the class NewFrame is not completely satisfactory:

  • NewFrame is not the logical place to deal with "dog" events.
  • It is better to place the method inside Dog.

We can use a static variable that will be shared across all Dog instances: (source file)

  • Declare the boolean variable race_over as a static variable in Dog:
    
    class Dog implements Runnable {
    
      // ... 
    
      static boolean race_over;
    
    }
        
  • Provide access to this variable via static synchronized methods:
    
    class Dog implements Runnable {
    
      // ... 
    
      public static synchronized void start_race ()
      {
        race_over = false;
      }
    
      public static synchronized boolean race_finished (boolean set, int ID)
      {
        // ... same as before ... 
      }
    
    }
        
  • This way, the static variable is shared across all the dog threads and access to it is synchronized.

Exercise 12.3 (Solution): Download the above source, and remove the keyword static in the declaration of race_finished(). The synchronization now does not work. Why?


Thread priorities

Java provides a range of priorities for threads:

  • Priorities range from 1 to 10 (although these numbers may change).

  • The lowest priority is the constant Thread.MIN_PRIORITY.

  • The highest priority is Thread.MAX_PRIORITY.

  • The normal (default) priority is Thread.NORM_PRIORITY.

  • You can set the priority of a thread using the instance method setPriority().

As an example, let us set different priorities for the two dogs in the method race(): (source file)


  void race ()
  {
    // ... 

    Thread d1_thread = new Thread (d1);
    d1_thread.setPriority (Thread.NORM_PRIORITY);
    Thread d2_thread = new Thread (d2);
    d2_thread.setPriority (Thread.NORM_PRIORITY+3);

    Dog.start_race();

    // Start running the threads. 
    // ("start" is a method in Thread). 
    d1_thread.start();
    d2_thread.start();

  }
  
We will also decrease the sleep time to the range 3-6 milliseconds:

	  Thread.sleep ((int)UniformRandom.uniform (3,6));
  

When executed, the second dog clearly wins. The screen output also indicates how often Dog 2 gets to execute.

Exercise 12.4 (Solution): Try using the old sleep range of 300-600 milliseconds. Now Dog 2 does not have much of an advantage. Why?


Waiting and notifying

Sometimes, threads need to coordinate on some activity:

  • Typically, a thread does some work and then needs to temporarily suspended while waiting for some condition to be true.

  • Once the condition is true, it needs to be woken up (notified) in order to continue execution.

  • Java provides ways by which threads can wait() by which one thread can notify() waiting threads.

The following is one approach to handling waiting and notification:

  • Define a new class to act as a monitor.

  • Then, place the data that determines waiting inside the monitor.

  • Allow access to this data only via synchronized methods in the monitor.

  • This way, only one thread will be executing inside the class at a time.

  • Use the wait() and notify() (or notifyAll()) methods of the monitor (inherited from Object) to achieve waiting and notification.

As an example, let us modify the dog race as follows:

  • When a dog sees that it is more than 20 paces (pixels) ahead of the other dog, it waits until the other comes within a distance of 20.
  • When a dog that is behind comes within 20 of the other dog, it notifies (wakes up) the other dog.

To achieve this objective, we will take the following steps:

  • We will create a monitor called DogMonitor:
    
    // A monitor class with synchronized access. 
    
    class DogMonitor {
    
      // Set the new position of Dog# ID. 
      public synchronized void set_position (int ID, int new_position)
      {
      }
    
      // Get the current position of Dog# ID. 
      public synchronized int get_position (int ID)
      {
      }
    
      // Wait. 
      public synchronized void synch_wait ()
      {
      }
    
      // Tell the other dog that it can continue. 
      public synchronized void synch_notify ()
      {
      }
    }
        
  • Then, we will modify the main loop of a Dog as follows:
    
        while (monitor.get_position (ID) < finish_line) {
    
          // Sleep. 
          
          // Move. 
          
          // If you need to wait, wait. 
    
          // If the other guy is waiting, notify. 
        } // endwhile 
    
        
  • To simplify the code, we will remove the feature of forcing the race to stop when one dog wins.

Here is the code: (source file)


class NewFrame extends Frame {

  // ...  

  public NewFrame ()
  {
    // ... same as before ... 
  }


  void race ()
  {
    Dimension D = c.getSize ();

    // Finish-line is at the right end of the canvas. 
    int finish_line = D.width;

    // Create the dog monitor. 
    DogMonitor monitor = new DogMonitor (2);

    // Create two dog instances with different ID's. 
    Dog d1 = new Dog (1, c, monitor, 2);
    Dog d2 = new Dog (2, c, monitor, 1);

    // Create a Thread instance for each dog. 
    // Note: the class Dog must implement the 
    // Runnable interface. 
    Thread d1_thread = new Thread (d1);
    Thread d2_thread = new Thread (d2);

    // Start running the threads. 
    // ("start" is a method in Thread). 
    d1_thread.start();
    d2_thread.start();
  }


}


// A monitor class with synchronized access. 

class DogMonitor {

  // The position of each dog. 
  int[] position;

  public DogMonitor (int num_dogs)
  {
    position = new int [num_dogs+1];
    for (int i=1; i<=num_dogs; i++)
      position[i] = 20;
  }

  // Set the new position of Dog# ID. 
  public synchronized void set_position (int ID, int new_position)
  {
    position[ID] = new_position;
  }

  // Get the current position of Dog# ID. 
  public synchronized int get_position (int ID)
  {
    return position[ID];
  }

  // A waiting dog needs to call a synchronized 
  // wait method inside the monitor. 
  public synchronized void synch_wait ()
  {
    try {
      // wait() is inherited from Object. 
      wait ();
    }
    catch (InterruptedException e)
    {
      System.out.println (e);
    }
  }

  // Tell the other dog that it can continue. 
  public synchronized void synch_notify ()
  {
    // notify() is inherited from Object. 
    notify ();
  }
}


class Dog implements Runnable {

  // ... 

  DogMonitor monitor;        // Store a reference to the monitor. 
  int OtherID;               // ID of the other dog. 

  public Dog (int ID, Canvas c, DogMonitor monitor, int OtherID)
  {
    // ... store parameters ... 
  }

  public void move ()
  {
    // ... same as before ... 
  }

  public void run ()
  {
    // Compute the finish line distance. 
    int finish_line = c.getSize().width;

    // While not complete... 
    while (monitor.get_position (ID) < finish_line) {
      try {
	Thread.sleep ((int)UniformRandom.uniform (300,600));
      }
      catch (InterruptedException e) {
	System.out.println (e);
      }
      
      move ();
      
      // Check if you need to wait. 
      int other = monitor.get_position (OtherID);
      int mine = monitor.get_position (ID);
      if (mine - other > 20) {
	System.out.println ("ID=" + ID + " waiting: mine=" + mine
			    + ", other=" + other);
	monitor.synch_wait ();
	System.out.println ("ID=" + ID + " continuing");
      }
      
      // See if the other guy is waiting for me. 
      if (other - mine <= 20) {
	System.out.println ("ID=" + ID + " notifying: mine=" + mine
			    + ", other=" + other);
	monitor.synch_notify();
      }
    } // endwhile 

  }

}


public class Race9 {
  // ... 
}
  

Note:

  • DogMonitor is a separate class, of which only one instance exists.

  • The accessors and mutators of DogMonitor are all synchronized.

  • The methods wait() and notify() are inherited from Object.

  • The use of these methods works as follows:
    • In Java, each object has an associated monitor (hidden from the programmer).
    • When a number of synchronized methods are defined in a class, only one thread at a time can execute a sychronized method inside the class.
    • Thus, suppose Thread A is currently executing the method set_position.
      Then, no other thread can execute any synchronized method in DogMonitor, even if the method is different.
    • Such a thread blocks (It is made to wait).
    • Thus, the hidden monitor associated with an object has a queue it uses to hold blocked threads.
    • When the currently executing thread completes, a thread is selected from those in the queue to continue execution.


  • A thread that executes inside a synchronized method can call the wait() method of the object to deliberately block itself:
    • It then gets placed on the queue of blocked threads.


  • A thread can also call the notify() method of the object:
    • This call will result in selecting the highest priority thread from the queue and allowing it to continue execution.
    • As an alternative, a thread can call notifyAll() to wake up all waiting threads.
    • Except for priorities, the programmer has no control over which thread is selected to continue execution.


  • Both wait() and notify() (or notifyAll()) must be called from inside synchronized methods, e.g.,
    
      public synchronized void synch_wait ()
      {
        try {
          // wait() is inherited from Object. 
          wait ();
        }
        catch (InterruptedException e)
        {
          System.out.println (e);
        }
      }
        
  • The method wait() requires a try-catch block.

Exercise 12.5 (Solution): Try executing the above code. What do you observe?


Deadlock

It is possible for the above code to deadlock.

Java does not detect deadlock for you and leaves it to you to worry about.

There are two ways to handle deadlock:

  • Avoid it altogether (the solution below).

  • Let it happen, detect it when it happens and break the deadlock:
    • Instead of calling wait() you can call the time-limited version wait (int milliseconds).
    • The thread is woken up after being blocked for that many milliseconds.
    • At this time, the thread should check to see if deadlock has occured.

We will provide a simple way around deadlock: always make sure no-one else is waiting when you call wait(): (source file)


// A monitor class with synchronized access. 

class DogMonitor {

  int[] position;
  boolean[] is_waiting;  // A waiting dog sets a value here. 
  int num_dogs;

  public DogMonitor (int num_dogs)
  {
    this.num_dogs = num_dogs;
    position = new int [num_dogs+1];
    for (int i=1; i<=num_dogs; i++)
      position[i] = 20;
    is_waiting = new boolean [num_dogs+1];
    for (int i=1; i<=num_dogs; i++)
      is_waiting [i] = false;
  }

  // Set the new position of Dog# ID. 
  public synchronized void set_position (int ID, int new_position)
  {
    position[ID] = new_position;
  }

  // Get the current position of Dog# ID. 
  public synchronized int get_position (int ID)
  {
    return position[ID];
  }

  // Before waiting, make sure no-one else waits. 
  public synchronized void synch_wait (int ID)
  {
    boolean someone_waiting = false;
    for (int i=1; i<=num_dogs; i++)
      if (is_waiting[i]) {
	System.out.println ("synch_wait: i=" + i + " is waiting");
	someone_waiting= true;
      }
    if (someone_waiting) {
      notifyAll();
      for (int i=1; i<=num_dogs; i++)
        is_waiting[i] = false;
    }

    try {
      is_waiting[ID] = true;
      wait ();
    }
    catch (InterruptedException e)
    {
      System.out.println (e);
    }
  }

  public synchronized void synch_notify ()
  {
    notify ();
  }
}
  

Note:

  • In method synch_wait(), we check to see if any other thread is waiting.

  • If so, those threads (all of them) are woken up by calling notifyAll().

  • This solution, while it works in this case (only two dogs), may not be appropriate in other circumstances.

  • Observe that the data in the main-loop of Dog is far from satisfactory since old data is being used:
    
        while (monitor.get_position (ID) < finish_line) {
          try {
    	Thread.sleep ((int)UniformRandom.uniform (300,600));
          }
          catch (InterruptedException e) {
    	System.out.println (e);
          }
          
          move ();
          
          // Check if you need to wait. 
          int other = monitor.get_position (OtherID);
          int mine = monitor.get_position (ID);
          if (mine - other > 20) {
    	System.out.println ("ID=" + ID + " waiting: mine=" + mine
    			    + ", other=" + other);
    	monitor.synch_wait (ID);
    	System.out.println ("ID=" + ID + " continuing");
          }
          
          // "other" AND "mine" COULD HAVE CHANGED BY NOW! 
    
          // See if the other guy is waiting for me. 
          if (other - mine <= 20) {
    	System.out.println ("ID=" + ID + " notifying: mine=" + mine
    			    + ", other=" + other);
    	monitor.synch_notify();
          }
        } // endwhile 
        
  • Thus, it is better to use synchronized methods to obtain the most recent values: (source file)
    
        while (monitor.get_position (ID) < finish_line) {
          try {
    	Thread.sleep ((int)UniformRandom.uniform (300,600));
          }
          catch (InterruptedException e) {
    	System.out.println (e);
          }
          
          move ();
          
          // Check if you need to wait. 
          if (monitor.get_position (ID) 
    	  - monitor.get_position (OtherID) > 20) {
    	monitor.synch_wait (ID);
          }
          
          // See if the other guy is waiting for me. 
          if (monitor.get_position (OtherID) 
    	  - monitor.get_position (ID) <= 20) {
    	monitor.synch_notify(ID);
          }
        } // endwhile 
        


Thread groups

NOTE: this section was written for Java 1.1. Many of the methods used are now deprecated. Refer to the current Swing version of this lecture for an updated version.

Sometimes it is desirable to bundle threads into a group for convenient "group" management.

This can be achieved quite easily using a ThreadGroup instance:

  • First, create a ThreadGroup instance.
  • Then, pass this instance to each Thread's constructor.
  • You can set group-properties and apply methods to the group as a whole.

As an example, we will include two additional buttons in our dog-race:

  • "Pause": to freeze or suspend the race.
  • "Continue": to continue a race after it has been frozen.
The methods suspend() and resume() of ThreadGroup can be used to suspend or resume the execution of threads in the group.

Here is the code: (source file)


class NewFrame extends Frame {

  // ... 

  public NewFrame ()
  {
    // ... 

    // A "pause" button. 
    Button pauseb = new Button ("PAUSE");
    pauseb.addActionListener (
      new ActionListener () {
        public void actionPerformed (ActionEvent a)
        {
	  suspend_race ();
	}
      }
    );
    p.add (pauseb);

    // A "continue" button. 
    Button continueb = new Button ("CONTINUE");
    continueb.addActionListener (
      new ActionListener () {
        public void actionPerformed (ActionEvent a)
        {
	  continue_race ();
	}
      }
    );
    p.add (continueb);

    // ... 
  }

  // ... 

  ThreadGroup DogGroup;

  void race ()
  {
    Dimension D = c.getSize ();

    // Finish-line is at the right end of the canvas. 
    int finish_line = D.width;

    // Create two dog instances with different ID's. 
    Dog d1 = new Dog (1, c);
    Dog d2 = new Dog (2, c);

    // Create a ThreadGroup instance. 
    // Note: a String must be passed as an ID. 
    DogGroup = new ThreadGroup ("All dogs");

    // Create a Thread instance for each dog. 
    Thread d1_thread = new Thread (DogGroup, d1);
    Thread d2_thread = new Thread (DogGroup, d2);

    Dog.start_race();

    // Start running the threads. 
    d1_thread.start();
    d2_thread.start();
  }

  public void suspend_race ()
  {
    DogGroup.suspend ();
  }

  public void continue_race ()
  {
    DogGroup.resume ();
  }

}
  

Note:

  • A ThreadGroup instance was created before creating the individual threads:
    
        DogGroup = new ThreadGroup ("All dogs");
        
  • Each Thread constructor is given the ThreadGroup instance as a parameter:
    
        Thread d1_thread = new Thread (DogGroup, d1);
        
  • suspend() and resume() are defined in ThreadGroup.


Summary

A few comments about threads:

  • Multithreading is relatively easy to use for completely independent one-shot activities, such as firing off a thread to handle an AWT event.

  • It is also natural to use for very large sub-applications, such as creating a thread for handling multiple connections to a socket.

  • Threading is a complex subject and we have barely scratched the surface.

  • Generally, you should avoid threads where possible or at least restrict its use to the minimum possible.

  • Waiting and notification greatly increase the complexity of threads. You really need to understand what you are doing if you use this mechanism.

  • Some other useful methods in Thread:
    • isAlive() - use this to check whether a thread is active.
    • join() - wait for a thread to complete execution.
    • sleep() - cause a particular thread to sleep. (Instance method).
    • yield() - be "nice" and let others run for a while.
    • interrupt() - interrupt a thread.
    • setDaemon() - make the thread a daemon:
      • A Java application completes execution when all its non-daemon threads have completed.
      • However, daemon threads continue until they complete.
      • After a thread has been started, its daemon status cannot be changed.


  • Some other useful methods in ThreadGroup:
    • activeCount() - how many threads are active in this group.
    • list() - print the threads in the group (to System.out).
    • resume(), stop(), suspend - all apply to the entire group.