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:
- A process
is a low-level operating system construct
used for multi-tasking.
- Processes run concurrently (but not necessarily in parallel).
- Processes run independently and do not share memory.
- Usually, processes can communicate, although it takes
some work (e.g., setting up "pipes") to make that happen.
- A process takes up resources, and it takes time (relatively
speaking) to switch processes.
- Processes are ideal for users, but can be expensive when
multiple processes are used for different activities within
a single program running for a particular user.
- Threads were designed to be used for concurrency within
a single application (run by a single user).
- Threads share memory (and, therefore, have fewer associated
resources).
- However, because memory is shared, the programmer has to
worry about threads that can corrupt data or behave strangely.
- It is easier to switch between threads than between processes,
- A special threads-package is required for operating systems that don't
support threads.
- Typically, if you are writing an application in which
activities need to take place concurrently (i.e., you don't
want the user to have to wait for them to finish serially),
you use threads.
About Java threads:
- Java provides the class Thread, the interface
Runnable and the reserved word synchronized
for thread programming.
- A Java thread is lightweight.
- The actual implementation varies across platforms since
an underlying OS-supplied thread package is used.
(Windows, Linux, and Mac-OS-X all have different behaviors).
- Here's what you need to be careful about:
- Some thread packages rotate between threads by giving
each thread a small slice of time.
- Other packages leave it to the threads to be "nice"
and volunteer to "sleep".
- Thus, if you have the latter package, you may need to
make each thread "nice" by placing a "sleep" call inside.
An example without threads
To see why threads are useful, let us first look
at an example that does not use threads:
- We will simulate a dog-race (the
Alaskan dog-sled race,
to be precise) viewed aerially:
two lines that grow randomly across the screen until one
reaches the end.
- Here's an example of what the screen looks like
in the middle of the race:
As you can see, it's a very crude depiction of a dog-race.
- Each dog sleeps for a random amount of time
(Uniformly distributed in [300,600] milliseconds).
- After waking up, a dog runs for a random distance
(Uniformly distributed in [50,100] pixels)
and goes to sleep again.
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:
- The basic components of the frame are: two buttons and
a canvas.
- Pressing the start button starts the race.
(This is done in the method race()).
- Each Dog instance:
- Keeps track of its position.
- Generates the next position and draws the ground covered so far.
- The simulation keeps track of the "next event" (i.e., which
dog wakes up next).
- In the while-loop of method race(), the
method sleep() of Thread is used:
- sleep() is a static method of Thread.
- Any code can call sleep().
- The parameter is the number of milliseconds the current
thread is put to sleep.
- sleep() throws an exception and therefore
requires a try-catch block.
- Just because we are calling
Thread.sleep()
does not mean that we are using threads.
- This just happens to be the class in which Java has placed
the
sleep()
method.
- The
sleep()
method "pauses" the current execution for the specified time.
- Note something interesting (troubling) about our application:
you cannot "quit" until the race is over:
- Thus, the event-handler for "quit" is not called until
the race completes.
- This is not the kind of behavior one expects of a
sophisticated application.
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:
- Each dog will run as a separate thread.
- Each such thread will sleep and move on its own.
- To create and use 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 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)