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

Solving deadlocks with globally-defined lock orderings

There are different ways to either minimize the likelihood of deadlocking or else just avoid it altogether. One best practice is to minimize the scope of your critical sections so as to decrease the time that threads hang onto locks, and hence to decrease the likelihood of a deadlock. That however doesn't solve the issue; deadlocks can still occur.

Another technique is to avoid the sort of nested locking that we saw in the example in the previous page. Instead of locking each account individually, we might define a single lock that any thread must acquire before doing a transfer. While this does indeed eliminate deadlocks, it does so at a very steep cost: acquiring the single lock becomes a bottleneck, and limits the parallelism we're trying to achieve in the first place. It does not scale.

A third technique is to use timeouts. Under this approach, if any thread is blocked for a sufficiently lengthy period of time, it just gives up.

Those are all legitimate techniques, but in this section we'll discuss global lock orders.

What is a globally-defined lock order?

Recall in our discussion of deadlocks that they arise from circular dependencies between the threads. Any given thread depends on a resource (a lock) that the previous thread in the ring holds. So one way to stamp out deadlocks is to define a global ordering on the locks so that such cycles can never arise. In other words, you define and publish rules around which locks to acquire before which other locks. This approach requires significant discipline from developers since there isn't any automatic way to enforce it. It is however extremely useful since it makes deadlocks impossible.

Let's see how we might apply a global lock order to the funds transfer example.

The funds transfer example reconsidered

At first glance you might think, "Wait a minute. The source account's lock is always grabbed before the destination account's lock, so how is it possible for a deadlock to occur?" But upon closer examination it's clear that that's not a correct analysis. Whatever global lock order we define needs to translate ultimately down to instances, not roles in a transaction. Sometimes account 4 will be the source account, and sometimes it will be the destination account. Similarly for account 14. Whatever global lock order we define needs to handle that.

One straightforward way to define a lock order for our current example would be to say that we always grab the lock for the account with the lower ID first. Listing 4 shows the new code:

Listing 4. Applying a globally-defined lock order
public void transferFunds(Account from, Account to, Money amount) {
	long fromId = from.getId();
	long toId = to.getId();
	long lowId = (fromId < toId ? fromId : toId);
	long highId = (fromId < toId ? toId : fromId);
	synchronized (lowId) {
		synchronized (highId) {
			from.decreaseBy(amount);
			to.increaseBy(amount);
		}
	}
}

The code above is deadlock-free, because cyclic dependencies are impossible. Say thread T1 wants to transfer funds from account 4 to account 14, and thread T2 wants to transfer funds from account 14 to account 4. Both threads will try to grab the lock for account 4 before trying to grab the lock for account 14. Thus it's impossible to wait for the lock on account 4 while holding the lock for account 14, at least as long as all code observes the same globally-defined lock ordering that we've observed here. (Hence "global".)

Conclusion and recommended readings

In this article we looked at several key areas of concurrent programming that (in my opinion, anyway) all practicing software developers should understand. In my observation, when it comes to concurrency, there is a huge gap between what typical developers should know and what they actually know, and that causes real-world production issues on a regular basis. I wrote this article to try to narrow that gap.

There are of course lots of good resources for concurrent programming. Here are some of my favorites:

  • Concurrency: Past and Present, by Brian Goetz: An entertaining presentation on concurrency. Goetz is a well-known authority on concurrent programming.
  • Java Concurrency in Practice, by Brian Goetz. A really great and approachable book.
  • Concurrent Programming in Java(TM): Design Principles and Pattern (2nd Edition), by Doug Lea. Another great book on concurrency; probably the standard. More academic than the Goetz book but still quite approachable.
  • The Therac-25 Accidents: A classic software engineering paper on how race conditions actually injured and killed people. The basic idea is that the Therac-25 machine, which was designed to administer radiation to cancer patients, was vulnerable to race conditions that occurred as the human operator became more experienced with operating the machine (and thus keyboarded commands more quickly). The race conditions led to massive radiation overdoses. Long but well worth the read.
Social bookmarks: del.icio.us Digg DZone Reddit StumbleUpon
« Previous | 1 | 2 | 3 | 4 | 5 | 6

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 Annotations RefCard
Check out the new DZone Spring Annotations Refcard by Craig Walls!

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.