Module 12: Threads


What is a thread?

 

Exercise 12.1: Look at your desktop applications and demonstrate concurrency using them: show that it is possible to have programs "do stuff" concurrently.
 

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:

 

Exercise 12.2: Download, compile and execute Race1.java. You will also need UniformRandom.java. Try clicking on "Quit" in the middle of the race. What happens? Examine the code to understand how the program works. Separately, examine and execute TestSleep.java to see an example of using "sleep" to pause execution.
 

Let us examine the code in more detail: (source file)

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

class NewFrame extends JFrame {

    // For drawing the race results. 
    JPanel drawingArea;


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

        Container cPane = this.getContentPane();
        // cPane.setLayout (new BorderLayout()); 

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

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

        // Now add the panel to the frame. 
        cPane.add (p, BorderLayout.SOUTH);

        // A JPanel to draw the results. 
        drawingArea = new JPanel();
        drawingArea.setBackground (Color.white);
        cPane.add (drawingArea, BorderLayout.CENTER);

        this.setVisible (true);
    }


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

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

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

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

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

        // Stop when one dog crosses the finish line. 

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

            // See which one is done first. 
            if (d1Nextwakeup < d2Nextwakeup) {

                // Dog d1 is first 
	        try {
	            // Static method "sleep" in class Thread. 
	            Thread.sleep (d1Nextwakeup - currentTime);
	        }
	        catch (InterruptedException e) {
	            System.out.println (e);
	        }
	        currentTime = d1Nextwakeup;
 	        d1.move(); 	            // Move a random distance. 
	        d1Nextwakeup += (int) UniformRandom.uniform (300,600);

            }
            else {

                // Dog d2 is first 
	        try {
	            Thread.sleep (d2Nextwakeup - currentTime);
	        }
	        catch (InterruptedException e) {
	            System.out.println (e);
	        }
	        currentTime = d2Nextwakeup;
	        d2.move();                  // Move a random distance. 
	        d2Nextwakeup += (int) UniformRandom.uniform (300,600);
            }

        } // end-while 
    }

}


class Dog {

    public int position = 20;  // Starting position. 
    int ID;                    // An ID. 
    JPanel drawingArea;        // The panel on which to draw. 

    public Dog (int ID, JPanel drawingArea)
    {
        this.ID = ID;
        this.drawingArea = drawingArea;

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

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

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

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

Note:

 

Exercise 12.3: Modify the race() method in 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:

Here is the code using threads: (source file)

class NewFrame extends JFrame {

    JPanel drawingArea;

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


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

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

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

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

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

}


class Dog implements Runnable {

    // ... 

    public Dog (int ID, JPanel drawingArea)
    {
        // ... 
    }

    public void move ()
    {
        // ... 
    }

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

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

}


public class Race2 {
    // ... 
}
  

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 dThread1 = new Thread (d1);
        
  • To actually start the thread going, one calls the start() method of the thread instance:
        dThread1.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, drawingArea);
         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.4: Modify Race2.java to add a third dog to the race. You will also need 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 the 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 raceFinished() in our frame, with the following signature:
      public boolean raceFinished (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 JFrame {

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

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

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

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

        // Create a Thread instance for each dog. 
        Thread dThread1 = new Thread (d1);
        Thread dThread2 = new Thread (d2);

        raceOver = false;

        // Start running the threads. 
        dThread1.start();
        dThread2.start();
    }


    boolean raceOver;

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

    public boolean raceFinished (boolean set)
    {
        boolean returnVal = false;
        if (!set)
            returnVal = raceOver;
        else {
            raceOver = true;
            returnVal = raceOver;
        }
        return returnVal;
    }
}

class Dog implements Runnable {

    // ...  

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

    public Dog (int ID, JPanel drawingArea, NewFrame f)
    {
        this.ID = ID;
        this.drawingArea = drawingArea;

        // Draw ID on canvas. 
        Graphics g = drawingArea.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 finishLine = drawingArea.getSize().width;

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

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

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

}

// ...  
  

Note:

  • Now each thread calls the raceFinished() 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.
 

Exercise 12.5: Examine the code in the program above. What is the difference between calling raceFinished(true) and raceFinished(false)? Where are these calls used?
 


Synchronized access to shared data.

 

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

  public boolean raceFinished (boolean set, int ID)
  {
      boolean returnVal = raceOver; 

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

      System.out.println ("raceFinished(): endofcode: ID=" + ID + ", set=" + set);
      return returnVal;
  }
  
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.
 

Exercise 12.6: Download and execute Race4.java above. Examine both the drawing and the text output and identify what went wrong.
 

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:

raceFinished(): startofcode: ID=2, set=true
raceFinished(): startofcode: ID=1, set=false
raceFinished(): endofcode: ID=1, set=false
raceFinished(): startofcode: ID=1, set=false
raceFinished(): endofcode: ID=1, set=false
raceFinished(): startofcode: ID=1, set=true
raceFinished(): endofcode: ID=2, set=true
raceFinished(): 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 raceFinished() concurrently.

The problem is easy to solve:

  • Simply include the modifier synchronized in the method raceFinished(): (source file)
      public synchronized boolean raceFinished (boolean set, int ID)
      {
    
          // ... everything else stays the same ...  
    
      }
        
Upon execution, the screen output shows that only one thread executes inside the body of raceFinished():
raceFinished(): startofcode: ID=2, set=false
raceFinished(): endofcode: ID=2, set=false
raceFinished(): startofcode: ID=2, set=true
raceFinished(): endofcode: ID=2, set=true
raceFinished(): startofcode: ID=1, set=false
raceFinished(): 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 raceFinished() 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 raceOver as a static variable in Dog:
    class Dog implements Runnable {
    
        // ...  
    
        static boolean raceOver;
    
    }
        
  • Provide access to this variable via static synchronized methods:
    class Dog implements Runnable {
    
        // ...  
    
        public static synchronized void startRace ()
        {
            raceOver = false;
        }
    
        public static synchronized boolean raceFinished (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.7: Download the above source, and remove the keyword static in the declaration of raceFinished(). 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 dThread1 = new Thread (d1);
      dThread1.setPriority (Thread.NORM_PRIORITY);
      Thread dThread2 = new Thread (d2);
      dThread2.setPriority (Thread.NORM_PRIORITY+3);

      Dog.startRace();

      // Start running the threads.  
      dThread1.start();
      dThread2.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.8: 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 be 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() and 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 rules of the dog race as follows:

  • When a dog sees that it is more than 20 paces (pixels) ahead of the other dog, it sportingly 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 setPosition (int ID, int newPosition)
        {
        }
    
        // Get the current position of Dog# ID.  
        public synchronized int getPosition (int ID)
        {
        }
    
        // Wait.  
        public synchronized void synchWait ()
        {
        }
    
        // Tell the other dog that it can continue.  
        public synchronized void synchNotify ()
        {
        }
    }
        
  • Then, we will modify the main loop of a Dog as follows:
        while (monitor.getPosition (ID) < finishLine) {
    
            // Sleep.  
          
            // Move.  
          
            // If you need to wait, wait.  
    
            // If the other guy is waiting, notify.  
        } 
    
        
  • 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 JFrame {

    // ...   

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


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

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

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

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

        // Create a Thread instance for each dog.  
        Thread dThread1 = new Thread (d1);
        Thread dThread2 = new Thread (d2);

        // Start running the threads.  
        dThread1.start();
        dThread2.start();
    }

}


// A monitor class with synchronized access.  

class DogMonitor {

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

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

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

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

    // A waiting dog needs to call a synchronized  
    // wait method inside the monitor.  
    public synchronized void synchWait ()
    {
        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 synchNotify ()
    {
        // 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, JPanel drawingArea, DogMonitor monitor, int OtherID)
    {
        // ... store parameters ...  
    }

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

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

        // While not complete...  
        while (monitor.getPosition (ID) < finishLine) {
            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.getPosition (OtherID);
            int mine = monitor.getPosition (ID);
            if (mine - other > 20) {
 	        System.out.println ("ID=" + ID + " waiting: mine=" + mine
			    + ", other=" + other);
                monitor.synchWait ();
    	        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.synchNotify();
            }
        } // 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 synchronized method inside the class.
    • Thus, suppose Thread A is currently executing the method setPosition.
      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 synchWait ()
      {
          try {
              // wait() is inherited from Object.  
              wait ();
          }
          catch (InterruptedException e)
          {
              System.out.println (e);
          }
      }
        
  • The method wait() requires a try-catch block.
 

Exercise 12.9: Try executing the above code. What do you observe?
 


Deadlock

 

It is possible for the above code to deadlock.

  • Deadlock occurs when two threads wait on each other:

  • 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[] isWaiting;  // A waiting dog sets a value here.  
    int numDogs;

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

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

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

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

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

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

Note:

  • In method synchWait(), 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.getPosition (ID) < finishLine) {
          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.getPosition (OtherID);
          int mine = monitor.getPosition (ID);
          if (mine - other > 20) {
    	  System.out.println ("ID=" + ID + " waiting: mine=" + mine
    			    + ", other=" + other);
    	  monitor.synchWait (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.synchNotify();
          }
        } // endwhile  
        
  • Thus, it is better to use synchronized methods to obtain the most recent values: (source file)
        while (monitor.getPosition (ID) < finishLine) {
            try {
    	    Thread.sleep ((int)UniformRandom.uniform (300,600));
            }
            catch (InterruptedException e) {
    	    System.out.println (e);
            }
          
            move ();
          
            // Check if you need to wait.  
            if (monitor.getPosition (ID) 
    	    - monitor.getPosition (OtherID) > 20) {
    	    monitor.synchWait (ID);
            }
          
            // See if the other guy is waiting for me.  
            if (monitor.getPosition (OtherID) 
    	    - monitor.getPosition (ID) <= 20) {
    	    monitor.synchNotify(ID);
            }
        } // endwhile  
        
 


Thread groups

 

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.
 

CAUTION about stopping and starting threads:

  • Java 1.1 allowed threads and threadgroups to suspend, resume and stop threads at will.
  • This is dangerous because a suspended thread may hold on to a lock, or worse, allow unlocked access when suspended (or stopped).
  • Since Java 1.2, the suspend(), resume() and stop() methods have been deprecated.
  • We will show how to write safe versions below.
 

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.

To build safe suspend/resume:

  • We will have our Runnable class implement the SafeRunnable interface (which we have defined):
    interface SafeRunnable extends Runnable {
    
        public abstract void safeStop ();
    
        public abstract void safeResume ();
      
        public abstract void safeSuspend ();
        
    }
       
    The idea is: a thread handles its own resume/suspend behavior.

  • Next, here is how the class Dog implements the interface:
    class Dog implements SafeRunnable {
    
        // ... 
    
        // New variables: 
        boolean isAlive = true;        // For safeStop 
        boolean isSuspended = false;   // For safeResume/Suspend 
    
        // .... 
    
        public void run ()
        {
            // ... 
    
            while ( (isAlive) && (position < finishLine) ) {
    	    try {
    	        Thread.sleep ((int)UniformRandom.uniform (300,600));
    
                    // Check whether suspended. 
                    checkSuspended();
    	    }
    	    catch (InterruptedException e) {
    	        System.out.println (e);
    	    }
            }
    
            // ... 
      }
    
      // ... 
    
        public synchronized void safeStop ()
        {
            isAlive = false;
        }
      
        public synchronized void safeSuspend ()
        {
            isSuspended = true;
        }
      
        public synchronized void safeResume ()
        {
            isSuspended = false;
            this.notify();
        }
    
        private synchronized void checkSuspended ()
        {
            try {
                while ( (isSuspended) && (isAlive) )
                   // Block while suspended. 
                   this.wait(); 
            }
            catch (InterruptedException e) {
                System.out.println (e);
            }
        }
        
    }
    
      
  • Next, let us define a SafeThreadGroup class:
    class SafeThreadGroup {
    
      Vector safeThreads = new Vector();
    
      public void addThread (SafeRunnable s)
      {
          safeThreads.addElement (s);
      }
    
      public void safeStop ()
      {
          Enumeration e = safeThreads.elements();
          while (e.hasMoreElements()) {
              SafeRunnable s = (SafeRunnable) e.nextElement();
              s.safeStop();
          }
      }
    
      public void safeResume ()
      {
          Enumeration e = safeThreads.elements();
          while (e.hasMoreElements()) {
              SafeRunnable s = (SafeRunnable) e.nextElement();
              s.safeResume();
          }
      }
      
      public void safeSuspend ()
      {
          Enumeration e = safeThreads.elements();
          while (e.hasMoreElements()) {
              SafeRunnable s = (SafeRunnable) e.nextElement();
              s.safeSuspend();
          }
      }
    
    }
      
  • Finally, here is the code in the frame:
    class NewFrame extends JFrame {
    
        // ... 
    
        public NewFrame ()
        {
            // ... 
    
            JButton pauseb = new JButton ("PAUSE");
            pauseb.addActionListener (
                new ActionListener () {
                    public void actionPerformed (ActionEvent a)
                    {
    	            suspendRace ();
    	        }
                }
            );
            p.add (pauseb);
    
            JButton continueb = new JButton ("CONTINUE");
                continueb.addActionListener (
                    new ActionListener () {
                        public void actionPerformed (ActionEvent a)
                        {
               	        continueRace ();
    	            }
                    }
            );
            p.add (continueb);
    
            // ... 
    
        }
    
    
        SafeThreadGroup dogGroup;
    
        void race ()
        {
            // ... 
        
            // Create two dog instances with different ID's. 
            Dog d1 = new Dog (1, drawingArea);
            Dog d2 = new Dog (2, drawingArea);
    
            // Create a ThreadGroup instance. 
            dogGroup = new SafeThreadGroup ();
    
            // Create a Thread instance for each dog. 
            // Note: the class Dog must implement the 
            // Runnable interface. 
            Thread dThread1 = new Thread (d1);
            Thread dThread2 = new Thread (d2);
    
            // Add the threads to the thread group. 
            dogGroup.addThread (d1);
            dogGroup.addThread (d2);
    
            Dog.startRace();
    
            // Start running the threads. 
            dThread1.start();
            dThread2.start();
        }
    
        public void suspendRace ()
        {
            dogGroup.safeSuspend ();
        }
    
        public void continueRace ()
        {
            dogGroup.safeResume ();
        }
    
    }
      
  • The complete source is available here.
 


Threads and Swing

 
About Swing and threads:

  • Swing uses a single thread to build GUIs, draw, and handle events.

  • If one is not careful, this can cause a serious problem.
 

Exercise 12.10: Download and execute SwingThread.java:

  • Initially, do NOT click on the task-2 button, but click on task-1 several times.
  • Next, click on task-2 and immediately try task-1 repeatedly. How soon after clicking on task-2 are you able to then click on task-1?
Now do the same thing with SwingThread2.java.
 

Let's examine the code: (source file)


class DrawPanel extends JPanel {

    // A simple panel extension that draws a circle of a given size.

}


public class SwingThread extends JFrame {

    DrawPanel drawPanel = new DrawPanel ();


    public SwingThread ()
    {
        this.setSize (600,600);
        
        // Button for Task 1:
        JPanel topPanel = new JPanel ();
        JButton first = new JButton ("Task 1");
        first.addActionListener (
            new ActionListener ()
            {
                public void actionPerformed (ActionEvent a)
                {
                    task1 ();
                }
            }
        );
        topPanel.add (first);

        // Button for Task 2:
        JButton second = new JButton ("Task 2");
        second.addActionListener (
            new ActionListener ()
            {
                public void actionPerformed (ActionEvent a)
                {
                    task2 ();
                }
            }
        );
        topPanel.add (second);

        // ... Quit button etc ...

        this.setVisible (true);
    }
    

    void task1 ()
    {
        // Increase the circle size and re-draw.
        drawPanel.circleSize += 10;
        drawPanel.repaint ();
    }


    void task2 ()
    {
        heavyComputation ();
    }
    

    void heavyComputation ()
    {
        // We'll just fake heavy computation by sleeping.
        try {
            for (int i=1; i<=10; i++) {
                drawPanel.msg = "Task 2: " + i;
                Thread.sleep (1000);
                drawPanel.repaint ();
            }
            drawPanel.msg = "";
            drawPanel.repaint ();
        }
        catch (InterruptedException e){
        }
    }


    public static void main (String[] argv)
    {
        new SwingThread ();
    }

}

  

Note:

  • Since Swing's thread is executing in heavyComputation() the thread has no time to respond to anything else.

  • The real problem: Swing's thread should be doing "swing things" instead of working on anything else.
 
To fix the problem:
  • We need to create a thread for every task and not let Swing's thread be consumed with our tasks.

  • Accordingly, we'll create and fire off a thread for the heavyComputation() task above.

Here's the code: (source file)


public class SwingThread2 extends JFrame {

    // ...

    public SwingThread2 ()
    {
        // ...
    }
    

    void task2 ()
    {
        // Make a thread.
        Thread thread = new Thread () {
            public void run () 
            {
                heavyComputation ();
            }
        };
 
        // Fire it off.
        thread.start ();

        // ... Thread ends when heavyComputation() completes execution.
    }
    

    void heavyComputation ()
    {
        // ... 
    }


    // ... 

}
  
 


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 - 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).



© 1998, Rahul Simha (revised 2017)