Despite the use of synchronized
statements, no synchronization takes place. Why? Examine synchronized (this)
. Because keyword this
refers to the current object, the deposit thread attempts to acquire the lock associated with the TransThread
object whose reference initially assigns to tt1
(in the main()
method). Similarly, the withdrawal thread attempts to acquire the lock associated with the TransThread
object whose reference initially assigns to tt2
. We have two different TransThread
objects, and each thread attempts to acquire the lock associated with its respective TransThread
object before entering its own critical code section. Because the threads acquire different locks, both threads can be in their own critical code sections at the same time. The result is no synchronization.
Tip: To avoid a no-synchronization scenario, choose an object common to all relevant threads. That way, those threads compete to acquire the same object's lock, and only one thread at a time can enter the associated critical code section.
Deadlock
In some programs, the following scenario might occur: Thread A acquires a lock that thread B needs before thread B can enter B's critical code section. Similarly, thread B acquires a lock that thread A needs before thread A can enter A's critical code section. Because neither thread has the lock it needs, each thread must wait to acquire its lock. Furthermore, because neither thread can proceed, neither thread can release the other thread's lock, and program execution freezes. This behavior is known as deadlock, which Listing 5 demonstrates:
Listing 5. DeadlockDemo.java
// DeadlockDemo.java
class DeadlockDemo
{
public static void main (String [] args)
{
FinTrans ft = new FinTrans ();
TransThread tt1 = new TransThread (ft, "Deposit Thread");
TransThread tt2 = new TransThread (ft, "Withdrawal Thread");
tt1.start ();
tt2.start ();
}
}
class FinTrans
{
public static String transName;
public static double amount;
}
class TransThread extends Thread
{
private FinTrans ft;
private static String anotherSharedLock = "";
TransThread (FinTrans ft, String name)
{
super (name); // Save thread's name
this.ft = ft; // Save reference to financial transaction object
}
public void run ()
{
for (int i = 0; i < 100; i++)
{
if (getName ().equals ("Deposit Thread"))
{
synchronized (ft)
{
synchronized (anotherSharedLock)
{
ft.transName = "Deposit";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 2000.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
}
else
{
synchronized (anotherSharedLock)
{
synchronized (ft)
{
ft.transName = "Withdrawal";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 250.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
}
}
}
}
If you run DeadlockDemo
, you will probably see only a single line of output before the application freezes. To unfreeze DeadlockDemo
, press Ctrl-C (assuming you are using Sun's SDK 1.4 toolkit at a Windows command prompt).
What causes the deadlock? Look carefully at the source code; the deposit thread must acquire two locks before it can enter its innermost critical code section. The outer lock associates with the FinTrans
object that ft
references, and the inner lock associates with the String
object that anotherSharedLock
references. Similarly, the withdrawal thread must acquire two locks before it can enter its own innermost critical code section. The outer lock associates with the String
object that anotherSharedLock
references, and the inner lock associates with the FinTrans
object that ft
references. Suppose both threads' execution orders are such that each thread acquires its outer lock. Thus, the deposit thread acquires its FinTrans
lock, and the withdrawal thread acquires its String
lock. Now that both threads possess their outer locks, they are in their appropriate outer critical code section. Both threads then attempt to acquire the inner locks, so they can enter the appropriate inner critical code sections.
The deposit thread attempts to acquire the lock associated with the anotherSharedLock
-referenced object. However, the deposit thread must wait because the withdrawal thread holds that lock. Similarly, the withdrawal thread attempts to acquire the lock associated with the ft
-referenced object. But the withdraw thread cannot acquire that lock because the deposit thread (which is waiting) holds it. Therefore, the withdrawal thread must also wait. Neither thread can proceed because neither thread releases the lock it holds. And neither thread can release the lock it holds because each thread is waiting. Each thread deadlocks, and the program freezes.
Tip: To avoid deadlock, carefully analyze your source code for situations where threads might attempt to acquire each others' locks, such as when a synchronized method calls another synchronized method. You must do that because a JVM cannot detect or prevent deadlock.
Review
To achieve strong performance with threads, you will encounter situations where your multithreaded programs need to serialize access to critical code sections. Known as synchronization, that activity prevents inconsistencies resulting in strange program behavior. You can use either synchronized
statements to guard portions of a method, or synchronize the entire method. But comb your code carefully for glitches that can result in failed synchronization or deadlocks.
In Part 3, I will introduce you to thread scheduling, thread interruption, and Java's wait/notify mechanism.
Learn more about this topic
- Download this article's source code and resource files.
- Read every article in the Understanding Java threads series (Jeff Friesen, JavaWorld.com, 2002):
- For a glossary specific to this article, homework, tips, and more, see the Java 101 study guide that accompanies this article.
- For more information about exceptions and the run() method, read "Handling Uncaught Exceptions" ( Glen McCluskey, Java Developer Connection, January 2001).
- The Programming Java Threads in the Real World series uses common programming scenarios to introduce readers to programming Java threads (Alan Holub, JavaWorld.com, 1998-2001):
- Part 1: A Java programmer's guide to threading architectures
- Part 2: The perils of race conditions, deadlock, and other threading problems
- Part 3: Roll-your-own mutexes and centralized lock management
- Part 4: Condition variables and counting semaphores—filling in a few chinks in Java's threading model)
- Part 5: Has Sun abandoned run anywhere? PlusThreads and Swing, timers, and getting around stop(), suspend(), and resume() deprecation)
- Part 6: The Observer pattern and mysteries of the AWTEventMulticaster
- Part 7: Singletons, critical sections, and reader/writer locks
- Part 8: Threads in an object-oriented world, thread pools, implementing socket accept loops
This story, "Java 101: Understanding Java threads, Part 2: Thread synchronization" was originally published by JavaWorld.