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