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.

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.