del.icio.us Digg DZone Reddit StumbleUpon
Concurrent Programming for Practicing Software Engineers - Willie Wheeler
« Previous | 1 | 2 | 3 | 4 | 5 | 6 | Next »

How synchronization gives rise to deadlocks

We saw in the previous section how a thread will grab a lock just before entering a critical section, thereby forcing other threads to "block" (concurrency lingo for "wait for another thread to release a lock") until they can acquire the lock. Normally this blocking behavior is unremarkable. It is a standard part of concurrent programming since we're trying to protect data from data races.

If there's too much contention for locks, that can of course have a negative effect on performance since threads are sitting around waiting instead of doing work. But usually this just means subpar performance rather than hanging.

The pathological case of contention for locks is deadlocking. This is characterized by two or more threads becoming permanently hung. They are notoriously difficult to predict, avoid and handle, but it's not impossible. The secret to understanding deadlocks is to understand that they arise when threads wait for each other to release a lock in a cyclic fashion. The simplest case would be thread T1 waiting for T2 to release a lock, and T2 waiting for T1 to release a lock. Neither one can make forward progress. The next simplest case is T1 waiting for T2, T2 waiting for T3, and T3 waiting for T1. Now we have three deadlocked threads. Obviously the cycle can be of arbitrary length; all threads involved will be deadlocked.

Here's a diagram that shows how it works:

The cyclic nature of deadlocks
Figure 1. Deadlocks arise from circular dependencies between threads.

In figure 1, you can see that each thread holds the lock needed by the preceding thread. As the dependencies are circular, there is no way for any of the threads to make forward progress once something like the above happens. All five threads are hung—that is, deadlocked.

Photo credit: gladius

A furry aside

By now you might understand what I meant at the beginning of the article when I said that concurrent programming is kind of like an arms race. We start with single threads, but sometimes that's too slow, so we introduce multithreading. That causes a problem though, which is that the threads can interfere with one another when working with shared data. So we introduce turn-taking through mutexes (and through the synchronized keyword in particular). But that has the effect of limiting parallelism, and if we take that too far, we can end up sacrificing the parallelism (and performance) that we sought in the first place. In the most extreme case, we can inadvertently cause threads to deadlock.

(Maybe Whac-A-Mole is a better metaphor. That's OK. I'm going to stick with arms race just because I really like the mech/cyber photo. Think of it as an arms race between software engineers and small furry rodents.)

Now let's consider an example of code vulnerable to deadlocks.

Some deadlocky code

For this one we're going to use the standard bank account funds transfer example. Yes, it's a clichéd example, but it's such a good example and originality is overrated anyway. Here's listing 3:

Listing 3. A deadlock just waiting to happen
public void transferFunds(Account from, Account to, Money amount) {
	synchronized (from) {
		synchronized (to) {
			from.decreaseBy(amount);
			to.increaseBy(amount);
		}
	}
}

(We'll ignore exceptional conditions such as insufficient funds. Or, if you like, assume that decreaseBy() throws an unchecked InsufficientFundsException.)

If it isn't obvious, the method is a service method that transfers funds from one account to another account. Before going further, take a look at the code and see if you can figure out how the deadlock can occur. Keep in mind what we said about cyclic dependencies.

Here's how a deadlock can occur

Recall from the discussion above that deadlocks involve multiple threads. Each one grabs a lock that one of the other threads needs, and needs a lock that one of the other threads has. Here, we have two synchronized blocks, and each entry point corresponds to a place where a thread will try to grab a lock, maybe successfully and maybe not.

With that information it's straightforward to construct the sequence of events that leads to the deadlock:

  1. Thread T1 enters the method, with the source account being account 4 and the destination account being account 14. T1 is able to acquire the lock for account 4, but then there's a context switch before it can enter the second synchronized block.
  2. Thread T2 also enters the method, with the source account being account 14 and the destination account being account 4. T2 is able to acquire the lock for account 14, but then gets blocked when trying to acquire the lock for account 4. That stands to reason, since T1 has it. Context switch.
  3. T1 tries to grab the lock for account 14, but of course it can't since T2 has it. Threads T1 and T2 are now deadlocked.

That's a fairly intricate bit of logic, and developers new to concurrent programming may wonder how in the heck they are supposed to anticipate problems like that. They may also wonder whether this series of events is so implausible that is just isn't worth worrying about. To answer the second concern first, deadlocks most certainly do occur in real code. Keep in mind that although the situations that produce them may be implausible, the problem is that CPU clock speeds are pretty incredible, and so things that seem implausible suddenly become just a matter of time.

As to how to defend against deadlocks, we'll look at that right now.

Social bookmarks: del.icio.us Digg DZone Reddit StumbleUpon
« Previous | 1 | 2 | 3 | 4 | 5 | 6 | Next »

Comments (4)

Hi,

I like your article because you described the classic problems and solutions well. But there are some minor issues with the code:

1) the userMap in the InMemoryUserDao is not published safely and therefor relies on an external happens before rule to prevent reordering and visibility problems. This could lead to a NullPointerException when accessing the users reference. In most cases with constructors this is not an issue, but imho still a bad practice. It can be fixed by making users final. For more information see http://pveentjer.wordpress.com/2008/04/02/jmm-constructors-dont-have-visiblity-problems/

2) I understand that you are working towards a solution with the InMemoryUserDao and step by step solve the problem. But I would add the advice to less experienced readers that they should look at really concurrent structures like the ConcurrentMap. It has atomic operations for the check/act problems (like putIfAbsent) but implementations can also perform better because they use lock striping for example instead of a big lock.

3) Instead of using AND the internal synchronization of the SynchronizedMap AND the synchronization in the InMemoryUserDao, I would drop the SynchronizedMap completely and replace it by a standard Hashmap (make sure that all methods are going through the lock) It makes reasoning about locking easier. So if you can't use the ConcurrentMap (e.g. a non standard datastructure) just have one location where the locking is done.
By Peter Veentjer on Oct 13, 2008 at 9:01 AM PDT
Well-written clear article handling threading and concurrency. Outstanding at the conceptual level.

The implementation details will of course be language specific and pattern specific, and there exists more than one way to accomplish it.

The value to me in the article is the clear handling of the concepts of threading, concurrency, related issues, and strategies to manage them.
By Dave Milner on Oct 13, 2008 at 10:44 AM PDT
Great article Willie. C# programmers that may be interested in how to accomplish synchronized methods should look into decorating the methods with the MethodImpl attribute

[MethodImpl(MethodImplOptions.Synchronized)]

or just wrap the entire content of your method in a lock statement.

e.g.

public class MyClass
{
public void MyMethod()
{
lock(typeof(MyClass))
{
// contents of method
}
}
}
By Collin Couch on Oct 13, 2008 at 11:01 AM PDT
@Peter: Ah, good catch re #1. Yes, declaring users final will prevent consumer threads from seeing the DAO instance half-published (object created but state not initialized) by the publisher thread. I agree with both of your other points as well. In particular, I like your #3. Concurrency semantics are communicated through documentation more than through language features and so stylistic issues become important. Thanks for the feedback.
By Willie Wheeler on Oct 13, 2008 at 10:11 PM PDT

Post a comment

Your name:
Your e-mail address (won't be displayed):
Your web site (optional):
example: www.xyz.com
Your comment:
Preview:
By You
Please help us reduce comment spam:
Spring in Practice
My brother and I are writing Spring in Practice for Manning!

What's New?

2009-08-30 - Check out my two-part series on DZone: Spring Integration: A Hands-On Tutorial.
2009-03-25 - My new article Getting Started with Spring Batch 2.0 is available on DZone.
Home | Consulting | Tech Articles | Mailing List | Contact | Spring Blog
Copyright © 2008 Wheeler Software, LLC.