10.3 Deadlocks

Ensuring that resources are used correctly between threads is easy in Java. Usually, it just takes the use of the synchronized keyword before a method. Because Java makes it seem so easy and painless to coordinate thread access to resources, the synchronized keyword tends to get used liberally. Up to and including Java 1.1, this was the approach taken even by Sun. You can still see in the earlier defined classes (e.g., java.util.Vector) that all methods that update instance variables are synchronized. From JDK 1.2, the engineers at Sun became more aware of performance and are now careful to avoid synchronizing willy-nilly. Instead, many classes are built unsynchronized but are provided with synchronized wrappers (see the later section Section 10.4.1).

Synchronizing methods liberally may seem like good safe programming, but it is a sure recipe for reducing performance at best, and creating deadlocks at worst. The following Deadlock class illustrates the simplest form of a race condition leading to deadlock. Here, the class Deadlock is Runnable. The run( ) method just has a short half-second delay and then calls hello( ) on another Deadlock object. The problem comes from the combination of the following three factors:

  • Both run( ) and hello( ) are synchronized

  • There is more than one thread

  • The sequence of execution does not guarantee that monitors are locked and unlocked in correct order

The main( ) method accepts one optional parameter to set the delay in milliseconds between starting the two threads. With a parameter of 1000 (one second), there should be no deadlock. Table 10-1 summarizes what happens when the program runs without deadlock.

Table 10-1. Example not deadlocked

d1Thread activity

d1 monitor owned by

d2 monitor owned by

d2Thread activity

Acquire d1 monitor and execute d1.run( )

d1Thread [in d1.run( )]

None

 

Sleeping in d1.run( ) for 500 milliseconds

d1Thread [in d1.run( )]

None

 

Acquire d2 monitor and execute d2.hello( )

d1Thread [in d1.run( )]

d1Thread [in d2.hello( )]

 

Sleeping in d2.hello( ) for 1000 milliseconds

d1Thread [in d1.run( )]

d1Thread [in d2.hello( )]

 

Sleeping in d2.hello( ) for 1000 milliseconds

d1Thread [in d1.run( )]

d1Thread [in d2.hello( )]

Try to acquire d2 monitor to execute d2.run( ), but block as d2 monitor is owned by d1Thread

Exit d2.hello( ) and release d2 monitor

d1Thread [in d1.run( )]

None

Blocked until d2 monitor is released

Running final statements in d1.run( )

d1Thread [in d1.run( )]

d2Thread [in d2.run( )]

Finally acquire d2 monitor and execute d2.run( )

Exit d1.run( ) and release d1 monitor

None

d2Thread [in d2.run( )]

Sleeping in d2.run( ) for 500 milliseconds

 

d2Thread [in d1.hello( )]

d2Thread [in d2.run( )]

Acquire d1 monitor and execute d1.hello( )

 

d2Thread [in d1.hello( )]

d2Thread [in d2.run( )]

Sleeping in d1.hello( ) for 1000 milliseconds

 

None

d2Thread [in d2.run( )]

Exit d1.hello( ) and release d1 monitor

 

None

None

Exit d2.run( ) and release d2 monitor

With a parameter of 0 (no delay between starting threads), there should be deadlock on all but the most heavily loaded systems. The calling sequence is shown in Table 10-2; Figure 10-2 summarizes the difference between the two cases. The critical difference between the deadlocked and nondeadlocked cases is whether d1Thread can acquire a lock on the d2 monitor before d2Thread manages to acquire a lock on d2 monitor.

Table 10-2. Example deadlocked

d1Thread activity

d1 monitor owned by

d2 monitor owned by

d2Thread activity

Acquire d1 monitor and execute d1.run( )

d1Thread [in d1.run( )]

None

 

Sleeping in d1.run( ) for 500 milliseconds

d1Thread [in d1.run( )]

d2Thread [in d2.run( )]

Acquire d2 monitor and execute d2.run( )

Blocked trying to acquire d2 monitor while starting d2.hello( ), as d2Thread owns d2 monitor

d1Thread [in d1.run( )]

d2Thread [in d2.run( )]

Sleeping in d2.run( ) for 500 milliseconds

Blocked until d2 monitor is released

d1Thread [in d1.run( )]

d2Thread [in d2.run( )]

Blocked trying to acquire d1 monitor while startingd1.hello( ), as d1Thread owns d1 monitor

Blocked until d2 monitor is released

d1Thread [in d1.run( )]

d2Thread [in d2.run( )]

Blocked until d1 monitor is released

A heavily loaded system can delay the startup of d2Thread enough that the behavior executes in the same way as the first sequence. This illustrates an important issue when dealing with threads: different system loads can expose problems in the application and also generate different performance profiles. The situation is typically the reverse of this example, with a race condition not showing deadlocks on lightly loaded systems, while a heavily loaded system alters the application behavior sufficiently to change thread interaction and cause deadlock. Bugs like this are extremely difficult to track down.

The Deadlock class is defined as follows:

package tuning.threads;
   
public class Deadlock implements Runnable
{
  String me;
  Deadlock other;
   
  public synchronized void hello(  )
  {
    //print out hello from this thread then sleep one second.
    System.out.println(me + " says hello");
    try {Thread.sleep(1000);}
    catch (InterruptedException e) {  }
  }
   
  public void init(String name, Deadlock friend)
  {
    //We have a name, and a reference to the other Deadlock object
    //so that we can call each other
    me = name;
    other = friend;
  }
   
  public static void main(String args[  ])
  {
    //wait as long as the argument suggests (or use 20 ms as default)
    int wait = args.length =  = 0 ? 20 : Integer.parseInt(args[0]);
   
    Deadlock d1 = new Deadlock(  );
    Deadlock d2 = new Deadlock(  );
   
    //make sure the Deadlock objects know each other
    d1.init("d1", d2);
    d2.init("d2", d1);
   
    Thread d1Thread = new Thread(d1);
    Thread d2Thread = new Thread(d2);
   
    //Start the first thread, then wait as long as
    //instructed before starting the other
    d1Thread.start(  );
    try {Thread.sleep(wait);}
    catch (InterruptedException e) {  }
    d2Thread.start(  );
  }
   
  public synchronized void run(  )
  {
    //We say we're starting, then sleep half a second.
    System.out.println("Starting thread " + me);
    try {Thread.sleep(500);}
    catch (InterruptedException e) {  }
   
    //Then we say we're calling the other guy's hello(  ), and do so
    System.out.println("Calling hello from " + me + " to " + other.me);
    other.hello(  );
    System.out.println("Ending thread " + me);
  }
}
Figure 10-2. The difference between nondeadlocked and deadlocked execution
figs/JPT2.1002.gif