Home > Articles > Programming > Java

  • Print
  • + Share This
This chapter is from the book

13.6 Thread Transitions

Thread States

Understanding the life cycle of a thread is valuable when programming with threads. Threads can exist in different states. Just because a thread’s start() method has been called, it does not mean that the thread has access to the CPU and can start executing straight away. Several factors determine how it will proceed.

Figure 13.3 shows the states and the transitions in the life cycle of a thread.

  • New state

    A thread has been created, but it has not yet started. A thread is started by calling its start() method, as explained in Section 13.4.

  • Ready-to-run state

    A thread starts life in the Ready-to-run state (see p. 639).

  • Running state

    If a thread is in the Running state, it means that the thread is currently executing (see p. 639).

  • Dead state

    Once in this state, the thread cannot ever run again (see p. 650).

  • Non-runnable states

    A running thread can transit to one of the non-runnable states, depending on the circumstances. A thread remains in a non-runnable state until a special transition occurs. A thread does not go directly to the Running state from a non-runnable state, but transits first to the Ready-to-run state.

    The non-runnable states can be characterized as follows:

    • Sleeping: The thread sleeps for a specified amount of time (see p. 640).
    • Blocked for I/O: The thread waits for a blocking operation to complete (see p. 649).
    • Blocked for join completion: The thread awaits completion of another thread (see p. 647).
    • Waiting for notification: The thread awaits notification from another thread (see p. 640).
    • Blocked for lock acquisition: The thread waits to acquire the lock of an object (see p. 626).
Figure 13.3

Figure 13.3 Thread States

The Thread class provides the getState() method to determine the state of the current thread. The method returns a constant of type Thread.State (i.e., the type State is a static inner enum type declared in the Thread class). The correspondence between the states represented by its constants and the states shown in Figure 13.3 is summarized in Table 13.1.

Table 13.1. Thread States

Constant in the Thread.State enum type

State in Figure 13.3

Description of the thread

NEW

New

Created but not yet started.

RUNNABLE

Runnable

Executing in the JVM.

BLOCKED

Blocked for lock acquisition

Blocked while waiting for a lock.

WAITING

Waiting for notify, Blocked for join completion

Waiting indefinitely for another thread to perform a particular action.

TIMED_WAITING

Sleeping, Waiting for notify, Blocked for join completion

Waiting for another thread to perform an action for up to a specified time.

TERMINATED

Dead

Completed execution.

Various methods from the Thread class are presented next. Examples of their usage are presented in subsequent sections.

  • final boolean isAlive()

    This method can be used to find out if a thread is alive or dead. A thread is alive if it has been started but not yet terminated, i.e., it is not in the Dead state.

  • final int getPriority()
    final void setPriority(int newPriority)

    The first method returns the priority of a thread. The second method changes its priority. The priority set will be the minimum of the specified newPriority and the maximum priority permitted for this thread.

  • Thread.State getState()

    This method returns the state of this thread (see Table 13.1). It should be used for monitoring the state and not for synchronizing control.

  • static void yield()

    This method causes the current thread to temporarily pause its execution and, thereby, allow other threads to execute. It is up to the JVM to decide if and when this transition will take place.

  • static void sleep (long millisec) throws InterruptedException

    The current thread sleeps for the specified time before it becomes eligible for running again.

  • final void join() throws InterruptedException
    final void join(long millisec) throws InterruptedException

    A call to any of these two methods invoked on a thread will wait and not return until either the thread has completed or it is timed out after the specified time, respectively.

  • void interrupt()

    The method interrupts the thread on which it is invoked. In the Waiting-for-notification, Sleeping, or Blocked-for-join-completion states, the thread will receive an InterruptedException.

Example 13.4 illustrates transitions between thread states. A thread at (1) sleeps a little at (2) and then does some computation in a loop at (3), after which the thread terminates. The main() method monitors the thread in a loop at (4), printing the thread state returned by the getState() method. The output shows that the thread goes through the RUNNABLE state when the run() method starts to execute and then transits to the TIMED_WAITING state to sleep. On waking up, it computes the loop in the RUNNABLE state, and transits to the TERMINATED state when the run() method finishes.

Example 13.4. Thread States

public class ThreadStates {

  private static Thread t1 = new Thread("T1") {    // (1)
    public void run() {
      try {
        sleep(2);                                  // (2)
        for(int i = 10000; i > 0; i--);            // (3)
      } catch (InterruptedException ie){
        ie.printStackTrace();
      }
    }
  };

  public static void main(String[] args) {
    t1.start();
    while(true) {                                  // (4)
      Thread.State state = t1.getState();
      System.out.println(state);
      if (state == Thread.State.TERMINATED) break;
    }
  }
}

Possible output from the program:

RUNNABLE
TIMED_WAITING
...
TIMED_WAITING
RUNNABLE
...
RUNNABLE
TERMINATED

Thread Priorities

Threads are assigned priorities that the thread scheduler can use to determine how the threads will be scheduled. The thread scheduler can use thread priorities to determine which thread gets to run. The thread scheduler favors giving CPU time to the thread with the highest priority in the Ready-to-run state. This is not necessarily the thread that has been the longest time in the Ready-to-run state. Heavy reliance on thread priorities for the behavior of a program can make the program unportable across platforms, as thread scheduling is host platform–dependent.

Priorities are integer values from 1 (lowest priority given by the constant Thread.MIN_PRIORITY) to 10 (highest priority given by the constant Thread.MAX_PRIORITY). The default priority is 5 (Thread.NORM_PRIORITY).

A thread inherits the priority of its parent thread. The priority of a thread can be set using the setPriority() method and read using the getPriority() method, both of which are defined in the Thread class. The following code sets the priority of the thread myThread to the minimum of two values: maximum priority and current priority incremented to the next level:

myThread.setPriority(Math.min(Thread.MAX_PRIORITY, myThread.getPriority()+1));

The setPriority() method is an advisory method, meaning that it provides a hint from the program to the JVM, which the JVM is in no way obliged to honor. The method can be used to fine-tune the performance of the program, but should not be relied upon for the correctness of the program.

Thread Scheduler

Schedulers in JVM implementations usually employ one of the two following strategies:

  • Preemptive scheduling.

    If a thread with a higher priority than the current running thread moves to the Ready-to-run state, the current running thread can be preempted (moved to the Ready-to-run state) to let the higher priority thread execute.

  • Time-Sliced or Round-Robin scheduling.

    A running thread is allowed to execute for a fixed length of time, after which it moves to the Ready-to-run state to await its turn to run again.

It should be emphasised that thread schedulers are implementation- and platform-dependent; therefore, how threads will be scheduled is unpredictable, at least from platform to platform.

Running and Yielding

After its start() method has been called, the thread starts life in the Ready-to-run state. Once in the Ready-to-run state, the thread is eligible for running, i.e., it waits for its turn to get CPU time. The thread scheduler decides which thread runs and for how long.

Figure 13.4 illustrates the transitions between the Ready-to-Run and Running states. A call to the static method yield(), defined in the Thread class, may cause the current thread in the Running state to transit to the Ready-to-run state, thus relinquishing the CPU. If this happens, the thread is then at the mercy of the thread scheduler as to when it will run again. It is possible that if there are no threads in the Ready-to-run state, this thread can continue executing. If there are other threads in the Ready-to-run state, their priorities can influence which thread gets to execute.

Figure 13.4

Figure 13.4 Running and Yielding

As with the setPriority() method, the yield() method is also an advisory method, and therefore comes with no guarantees that the JVM will carry out the call’s bidding. A call to the yield() method does not affect any locks that the thread might hold.

By calling the static method yield(), the running thread gives other threads in the Ready-to-run state a chance to run. A typical example where this can be useful is when a user has given some command to start a CPU-intensive computation, and has the option of cancelling it by clicking on a CANCEL button. If the computation thread hogs the CPU and the user clicks the CANCEL button, chances are that it might take a while before the thread monitoring the user input gets a chance to run and take appropriate action to stop the computation. A thread running such a computation should do the computation in increments, yielding between increments to allow other threads to run. This is illustrated by the following run() method:

public void run() {
  try {
    while (!done()) {
      doLittleBitMore();
      Thread.yield();     // Current thread yields
    }
  } catch (InterruptedException ie) {
    doCleaningUp();
  }
}

Sleeping and Waking Up

Transitions by a thread to and from the Sleeping state are illustrated in Figure 13.5.

Figure 13.5

Figure 13.5 Sleeping and Waking up

A call to the static method sleep() in the Thread class will cause the currently running thread to temporarily pause its execution and transit to the Sleeping state. This method does not relinquish any lock that the thread might have. The thread will sleep for at least the time specified in its argument, before transitioning to the Ready-to-run state where it takes its turn to run again. If a thread is interrupted while sleeping, it will throw an InterruptedException when it awakes and gets to execute.

There are two overloaded versions of the sleep() method in the Thread class, allowing time to be specified in milliseconds, and additionally in nanoseconds.

Usage of the sleep() method is illustrated in Examples 13.1, 13.2, and 13.3.

Waiting and Notifying

Waiting and notifying provide means of communication between threads that synchronize on the same object (see Section 13.5, p. 626). The threads execute wait() and notify() (or notifyAll()) methods on the shared object for this purpose. These final methods are defined in the Object class and, therefore, inherited by all objects. These methods can only be executed on an object whose lock the thread holds (in other words, in synchronized code), otherwise, the call will result in an IllegalMonitorStateException.

  • final void wait(long timeout) throws InterruptedException
    final void wait(long timeout, int nanos) throws InterruptedException
    final void wait() throws InterruptedException

    A thread invokes the wait() method on the object whose lock it holds. The thread is added to the wait set of the current object.

  • final void notify()
    final void notifyAll()

    A thread invokes a notification method on the current object whose lock it holds to notify thread(s) that are in the wait set of the object.

Communication between threads is facilitated by waiting and notifying, as illustrated by Figures 13.6 and 13.7. A thread usually calls the wait() method on the object whose lock it holds because a condition for its continued execution was not met. The thread leaves the Running state and transits to the Waiting-for-notification state. There it waits for this condition to occur. The thread relinquishes ownership of the object lock.

Figure 13.6

Figure 13.6 Waiting and Notifying

Figure 13.7

Figure 13.7 Thread Communication

Transition to the Waiting-for-notification state and relinquishing the object lock are completed as one atomic (non-interruptible) operation. The releasing of the lock of the shared object by the thread allows other threads to run and execute synchronized code on the same object after acquiring its lock.

Note that the waiting thread relinquishes only the lock of the object on which the wait() method was invoked. It does not relinquish any other object locks that it might hold, which will remain locked while the thread is waiting.

Each object has a wait set containing threads waiting for notification. Threads in the Waiting-for-notification state are grouped according to the object whose wait() method they invoked.

Figure 13.7 shows a thread t1 that first acquires a lock on the shared object, and afterward invokes the wait() method on the shared object. This relinquishes the object lock and the thread t1 awaits to be notified. While the thread t1 is waiting, another thread t2 can acquire the lock on the shared object for its own purposes.

A thread in the Waiting-for-notification state can be awakened by the occurrence of any one of these three incidents:

  1. Another thread invokes the notify() method on the object of the waiting thread, and the waiting thread is selected as the thread to be awakened.
  2. The waiting thread times out.
  3. Another thread interrupts the waiting thread.

Notified

Invoking the notify() method on an object wakes up a single thread that is waiting for the lock of this object. The selection of a thread to awaken is dependent on the thread policies implemented by the JVM. On being notified, a waiting thread first transits to the Blocked-for-lock-acquisition state to acquire the lock on the object, and not directly to the Ready-to-run state. The thread is also removed from the wait set of the object. Note that the object lock is not relinquished when the notifying thread invokes the notify() method. The notifying thread relinquishes the lock at its own discretion, and the awakened thread will not be able to run until the notifying thread relinquishes the object lock.

When the notified thread obtains the object lock, it is enabled for execution, waiting in the Ready-to-run state for its turn to execute again. Finally, when it does execute, the call to the wait() method returns and the thread can continue with its execution.

From Figure 13.7 we see that thread t2 does not relinquish the object lock when it invokes the notify() method. Thread t1 is forced to wait in the Blocked-for-lock-acquisition state. It is shown no privileges and must compete with any other threads waiting for lock acquisition.

A call to the notify() method has no effect if there are no threads in the wait set of the object.

In contrast to the notify() method, the notifyAll() method wakes up all threads in the wait set of the shared object. They will all transit to the Blocked-for-lock-acquisition state and contend for the object lock as explained earlier.

It should be stressed that a program should not make any assumptions about the order in which threads awaken in response to the notify() or notifyAll() method and transit to the Blocked-for-lock-acquisition state.

Timed-out

The wait() call specified the time the thread should wait before being timed out, if it was not awakened by being notified. The awakened thread competes in the usual manner to execute again. Note that the awakened thread has no way of knowing whether it was timed out or woken up by one of the notification methods.

Interrupted

This means that another thread invoked the interrupt() method on the waiting thread. The awakened thread is enabled as previously explained, but the return from the wait() call will result in an InterruptedException if and when the awakened thread finally gets a chance to run. The code invoking the wait() method must be prepared to handle this checked exception.

Using Wait and Notify

In Example 13.5, three threads are manipulating the same stack. Two of them are pushing elements on the stack, while the third one is popping elements off the stack. The class diagram for Example 13.5 is shown in Figure 13.8. The example comprises the following classes:

  • The subclasses StackPopper at (9) and StackPusher at (10) extend the abstract superclass StackUser at (5).
  • Class StackUser, which extends the Thread class, creates and starts each thread.
  • Class StackImpl implements the synchronized methods pop() and push().
Figure 13.8

Figure 13.8 Stack Users

Again, the code in Example 13.5 has not been generified in order to keep things simple.

In Example 13.5, the field topOfStack in class StackImpl is declared volatile, so that read and write operations on this variable will access the master value of this variable, and not any copies, during runtime (see Section 4.10, p. 153).

Since the threads manipulate the same stack object, and the push() and pop() methods in the class StackImpl are synchronized, it means that the threads synchronize on the same object. In other words, the mutual exclusion of these operations is guaranteed on the same stack object.

Example 13.5. Waiting and Notifying

class StackImpl {
  private Object[] stackArray;
  private volatile int topOfStack;

  StackImpl (int capacity) {
    stackArray = new Object[capacity];
    topOfStack = -1;
  }

  public synchronized Object pop() {
    System.out.println(Thread.currentThread() + ": popping");
    while (isEmpty())
      try {
        System.out.println(Thread.currentThread() + ": waiting to pop");
        wait();                                      // (1)
      } catch (InterruptedException ie) {
        System.out.println(Thread.currentThread() + " interrupted.");
      }
    Object element = stackArray[topOfStack];
    stackArray[topOfStack--] = null;
    System.out.println(Thread.currentThread() +
                       ": notifying after popping");
    notify();                                        // (2)
    return element;
  }

  public synchronized void push(Object element) {
    System.out.println(Thread.currentThread() + ": pushing");
    while (isFull())
      try {
        System.out.println(Thread.currentThread() + ": waiting to push");
        wait();                                      // (3)
      } catch (InterruptedException ie) {
        System.out.println(Thread.currentThread() + " interrupted.");
      }
    stackArray[++topOfStack] = element;
    System.out.println(Thread.currentThread() +
                       ": notifying after pushing");
    notify();                                        // (4)
  }

  public boolean isFull() { return topOfStack >= stackArray.length -1; }
  public boolean isEmpty() { return topOfStack < 0; }
}
//_______________________________________________________________________________
abstract class StackUser implements Runnable {       // (5) Stack user

  protected StackImpl stack;                         // (6)

  StackUser(String threadName, StackImpl stack) {
    this.stack = stack;
    Thread worker = new Thread(this, threadName);
    System.out.println(worker);
    worker.setDaemon(true);                          // (7) Daemon thread status
    worker.start();                                  // (8) Start the thread
  }
}
//_______________________________________________________________________________
class StackPopper extends StackUser {                // (9) Popper
  StackPopper(String threadName, StackImpl stack) {
    super(threadName, stack);
  }
  public void run() { while (true) stack.pop(); }
}
//_______________________________________________________________________________
class StackPusher extends StackUser {                // (10) Pusher
  StackPusher(String threadName, StackImpl stack) {
    super(threadName, stack);
  }
  public void run() { while (true) stack.push(2008); }
}
//_______________________________________________________________________________
public class WaitAndNotifyClient {
  public static void main(String[] args)
                     throws InterruptedException {   // (11)

    StackImpl stack = new StackImpl(5);              // Stack of capacity 5.

    new StackPusher("A", stack);
    new StackPusher("B", stack);
    new StackPopper("C", stack);
    System.out.println("Main Thread sleeping.");
    Thread.sleep(10);
    System.out.println("Exit from Main Thread.");
  }
}

Possible output from the program:

Thread[A,5,main]
Thread[B,5,main]
Thread[C,5,main]
Main Thread sleeping.
...
Thread[A,5,main]: pushing
Thread[A,5,main]: waiting to push
Thread[B,5,main]: pushing
Thread[B,5,main]: waiting to push
Thread[C,5,main]: popping
Thread[C,5,main]: notifying after pop
Thread[A,5,main]: notifying after push
Thread[A,5,main]: pushing
Thread[A,5,main]: waiting to push
Thread[B,5,main]: waiting to push
Thread[C,5,main]: popping
Thread[C,5,main]: notifying after pop
Thread[A,5,main]: notifying after push
...
Thread[B,5,main]: notifying after push
...
Exit from Main Thread.
...

Example 13.5 illustrates how a thread waiting as a result of calling the wait() method on an object is notified by another thread calling the notify() method on the same object, in order for the first thread to start running again.

One usage of the wait() call is shown in Example 13.5 at (1) in the synchronized pop() method. When a thread executing this method on the StackImpl object finds that the stack is empty, it invokes the wait() method in order to wait for some thread to push something on this stack first.

Another use of the wait() call is shown at (3) in the synchronized push() method. When a thread executing this method on the StackImpl object finds that the stack is full, it invokes the wait() method to await some thread removing an element first, in order to make room for a push operation on the stack.

When a thread executing the synchronized method push() on the StackImpl object successfully pushes an element on the stack, it calls the notify() method at (4). The wait set of the StackImpl object contains all waiting threads that have earlier called the wait() method at either (1) or (3) on this StackImpl object. A single thread from the wait set is enabled for running. If this thread was executing a pop operation, it now has a chance of being successful because the stack is not empty at the moment. If this thread was executing a push operation, it can try again to see if there is room on the stack.

When a thread executing the synchronized method pop() on the StackImpl object successfully pops an element off the stack, it calls the notify() method at (2). Again assuming that the wait set of the StackImpl object is not empty, one thread from the set is arbitrarily chosen and enabled. If the notified thread was executing a pop operation, it can proceed to see if the stack still has an element to pop. If the notified thread was executing a push operation, it now has a chance of succeeding, because the stack is not full at the moment.

Note that the waiting condition at (1) for the pop operation is executed in a loop. A waiting thread that has been notified is not guaranteed to run right away. Before it gets to run, another thread may synchronize on the stack and empty it. If the notified thread was waiting to pop the stack, it would now incorrectly pop the stack, because the condition was not tested after notification. The loop ensures that the condition is always tested after notification, sending the thread back to the Waiting-on-notification state if the condition is not met. To avert the analogous danger of pushing on a full stack, the waiting condition at (3) for the push operation is also executed in a loop.

The behavior of each thread can be traced in the output from Example 13.5. Each push-and-pop operation can be traced by a sequence consisting of the name of the operation to be performed, followed by zero or more wait messages, and concluding with a notification after the operation is done. For example, thread A performs two pushes as shown in the output from the program:

Thread[A,5,main]: pushing
Thread[A,5,main]: waiting to push
...
Thread[A,5,main]: notifying after push
Thread[A,5,main]: pushing
Thread[A,5,main]: waiting to push
...
Thread[A,5,main]: notifying after push

Thread B is shown doing one push:

Thread[B,5,main]: pushing
Thread[B,5,main]: waiting to push
...
Thread[B,5,main]: notifying after push

Whereas thread C pops the stack twice without any waiting:

Thread[C,5,main]: popping
Thread[C,5,main]: notifying after pop
...
Thread[C,5,main]: popping
Thread[C,5,main]: notifying after pop

When the operations are interweaved, the output clearly shows that the pushers wait when the stack is full, and only push after the stack is popped.

The three threads created are daemon threads. Their status is set at (7). They will be terminated if they have not completed when the main thread dies, thereby stopping the execution of the program.

Joining

A thread can invoke the overloaded method join() on another thread in order to wait for the other thread to complete its execution before continuing, i.e., the first thread waits for the second thread to join it after completion. A running thread t1 invokes the method join() on a thread t2. The join() call has no effect if thread t2 has already completed. If thread t2 is still alive, thread t1 transits to the Blocked-for-join-completion state. Thread t1 waits in this state until one of these events occur (see Figure 13.9):

  • Thread t2 completes.

    In this case thread t1 moves to the Ready-to-run state, and when it gets to run, it will continue normally after the call to the join() method.

  • Thread t1 is timed out.

    The time specified in the argument of the join() method call has elapsed without thread t2 completing. In this case as well, thread t1 transits to the Ready-to-run state. When it gets to run, it will continue normally after the call to the join() method.

  • Thread t1 is interrupted.

    Some thread interrupted thread t1 while thread t1 was waiting for join completion. Thread t1 transits to the Ready-to-run state, but when it gets to execute, it will now throw an InterruptedException.

Figure 13.9

Figure 13.9 Joining of Threads

Example 13.6 illustrates joining of threads. The AnotherClient class below uses the Counter class, which extends the Thread class from Example 13.2. It creates two threads that are enabled for execution. The main thread invokes the join() method on the Counter A thread. If the Counter A thread has not already completed, the main thread transits to the Blocked-for-join-completion state. When the Counter A thread completes, the main thread will be enabled for running. Once the main thread is running, it continues with execution after (5). A parent thread can call the isAlive() method to find out whether its child threads are alive before terminating itself. The call to the isAlive() method on the Counter A thread at (6) correctly reports that the Counter A thread is not alive. A similar scenario transpires between the main thread and the Counter B thread. The main thread passes through the Blocked-for-join-completion state twice at the most.

Example 13.6. Joining of Threads

class Counter extends Thread { /* See Example 13.2. */ }
//_______________________________________________________________________________
public class AnotherClient {
  public static void main(String[] args) {

    Counter counterA = new Counter("Counter A");
    Counter counterB = new Counter("Counter B");

    try {
      System.out.println("Wait for the child threads to finish.");
      counterA.join();                                 // (5)
      if (!counterA.isAlive())                         // (6)
        System.out.println("Counter A not alive.");
      counterB.join();                                 // (7)
      if (!counterB.isAlive())                         // (8)
        System.out.println("Counter B not alive.");
    } catch (InterruptedException ie) {
      System.out.println("Main Thread interrupted.");
    }
    System.out.println("Exit from Main Thread.");
  }
}

Possible output from the program:

Thread[Counter A,5,main]
Thread[Counter B,5,main]
Wait for the child threads to finish.
Counter A: 0
Counter B: 0
Counter A: 1
Counter B: 1
Counter A: 2
Counter B: 2
Counter A: 3
Counter B: 3
Counter A: 4
Counter B: 4
Exit from Counter A.
Counter A not alive.
Exit from Counter B.
Counter B not alive.
Exit from Main Thread.

Blocking for I/O

A running thread, on executing a blocking operation requiring a resource (like a call to an I/O method), will transit to the Blocked-for-I/O state. The blocking operation must complete before the thread can proceed to the Ready-to-run state. An example is a thread reading from the standard input terminal which blocks until input is provided:

int input = System.in.read();

Thread Termination

A thread can transit to the Dead state from the Running or the Ready-to-run states. The thread dies when it completes its run() method, either by returning normally or by throwing an exception. Once in this state, the thread cannot be resurrected. There is no way the thread can be enabled for running again, not even by calling the start() method again on the thread object.

Example 13.7 illustrates a typical scenario where a thread can be controlled by one or more threads. Work is performed by a loop body, which the thread executes continually. It should be possible for other threads to start and stop the worker thread. This functionality is implemented by the class Worker at (1), which has a private field theThread declared at (2) to keep track of the Thread object executing its run() method.

The kickStart() method at (3) in class Worker creates and starts a thread if one is not already running. It is not enough to just call the start() method on a thread that has terminated. A new Thread object must be created first. The terminate() method at (4) sets the field theThread to null. Note that this does not affect any Thread object that might have been referenced by the reference theThread. The runtime system maintains any such Thread object; therefore, changing one of its references does not affect the object.

The run() method at (5) has a loop whose execution is controlled by a special condition. The condition tests to see whether the Thread object referenced by the reference theThread and the Thread object executing now, are one and the same. This is bound to be the case if the reference theThread has the same reference value that it was assigned when the thread was created and started in the kickStart() method. The condition will then be true, and the body of the loop will execute. However, if the value in the reference theThread has changed, the condition will be false. In that case, the loop will not execute, the run() method will complete and the thread will terminate.

A client can control the thread implemented by the class Worker, using the kickStart() and the terminate() methods. The client is able to terminate the running thread at the start of the next iteration of the loop body by calling the terminate() method that changes the value of the theThread reference to null.

In Example 13.7, a Worker object is first created at (8) and a thread started on this Worker object at (9). The main thread invokes the sleep() method at (10) to temporarily cease its execution for 2 milliseconds giving the thread of the Worker object a chance to run. The main thread, when it is executing again, terminates the thread of the Worker object at (11), as explained earlier. This simple scenario can be generalized where several threads, sharing a single Worker object, could be starting and stopping the thread of the Worker object.

Example 13.7. Thread Termination

class Worker implements Runnable {                              // (1)
  private volatile Thread theThread;                            // (2)

  public void kickStart() {                                     // (3)
    if (theThread == null) {
      theThread = new Thread(this);
      theThread.start();
    }
  }

  public void terminate() {                                     // (4)
    theThread = null;
  }

  public void run() {                                           // (5)
    while (theThread == Thread.currentThread()) {               // (6)
      System.out.println("Going around in loops.");
    }
  }
}
//_______________________________________________________________________________
public class Controller {
  public static void main(String[] args) {                      // (7)
    Worker worker = new Worker();                               // (8)
    worker.kickStart();                                         // (9)
    try {
      Thread.sleep(2);                                          // (10)
    } catch(InterruptedException ie) {
      ie.printStackTrace();
    }
    worker.terminate();                                         // (11)
  }
}

Possible output from the program:

Going around in loops.
Going around in loops.
Going around in loops.
Going around in loops.
Going around in loops.

Deadlocks

A deadlock is a situation where a thread is waiting for an object lock that another thread holds, and this second thread is waiting for an object lock that the first thread holds. Since each thread is waiting for the other thread to relinquish a lock, they both remain waiting forever in the Blocked-for-lock-acquisition state. The threads are said to be deadlocked.

A deadlock is depicted in Figure 13.10. Thread t1 has a lock on object o1, but cannot acquire the lock on object o2. Thread t2 has a lock on object o2, but cannot acquire the lock on object o1. They can only proceed if one of them relinquishes a lock the other one wants, which is never going to happen.

Figure 13.10

Figure 13.10 Deadlock

The situation in Figure 13.10 is implemented in Example 13.8. Thread t1 at (3) tries to synchronize at (4) and (5), first on string o1 at (1) then on string o2 at (2), respectively. The thread t2 at (6) does the opposite. It synchronizes at (7) and (8), first on string o2 then on string o1, respectively. A deadlock can occur as explained previously.

However, the potential of deadlock in the situation in Example 13.8 is easy to fix. If the two threads acquire the locks on the objects in the same order, then mutual lock dependency is avoided and a deadlock can never occur. This means having the same locking order at (4) and (5) as at (7) and (8). In general, the cause of a deadlock is not always easy to discover, let alone easy to fix.

Example 13.8. Deadlock

public class DeadLockDanger {

  String o1 = "Lock " ;                        // (1)
  String o2 = "Step ";                         // (2)

  Thread t1 = (new Thread("Printer1") {        // (3)
    public void run() {
      while(true) {
        synchronized(o1) {                     // (4)
          synchronized(o2) {                   // (5)
            System.out.println(o1 + o2);
          }
        }
      }
    }
  });

  Thread t2 = (new Thread("Printer2") {        // (6)
    public void run() {
      while(true) {
        synchronized(o2) {                     // (7)
          synchronized(o1) {                   // (8)
            System.out.println(o2 + o1);
          }
        }
      }
    }
  });

  public static void main(String[] args) {
    DeadLockDanger dld = new DeadLockDanger();
    dld.t1.start();
    dld.t2.start();
  }
}

Possible output from the program:

...
Lock Step
Lock Step
Lock Step
Lock Step
Lock Step
_
  • + Share This
  • 🔖 Save To Your Account