Home > Software Engineering > The Hidden Danger of Synchronization

The Hidden Danger of Synchronization

February 20th, 2009 John Leave a comment Go to comments

padlock

A programmer realised he had a problem which could be solved using threads.
Now he has two problems.

This isn’t going to be an anti-thread post, but I do want to sound a few words of caution about threads, and, specifically, synchronizers and monitors, and the order in which you acquire them.

Threads are a solution to a number of problems.    Java threads are easy to use and (since we got rid of ‘green’ threads and got real ones instead) become real O/S threads, mapping onto available processor cores as required.  This means that any tasks which can be parallelised, where speed of completion is important, or jobs which exhibit properties allowing the job space to be split and processed/merged separately, are prime targets for threading and the inherent speedup of being farmed out to parallel cores.

This will become ever more important as processors stop becoming faster (in fact I think this point has already been reached – Intel’s Core i7’s clock speed is still only 3.20 GHz, clever pipelining notwithstanding), and start gaining massively more cores.  The more parallelization you can find in tasks, the faster your total execution.

The problem with threads is synchronizers. As engineers we try to avoid synchronizers if possible because of the inherent cost of the JVM processing them (especially inflated ’slow’ synchronizers), but there’s no denying the mechanism is useful.

The old tenets of monitor locking still apply:

  • If you can get away with not doing it and still produce correct code  – don’t do it
  • Reduce the scope of the monitor acquisition as far as possible
  • Obtain monitors in the same order all over the code

The last point is the one which is mostly likely to cause problems.  It’s easy to find problems if you have code which looks like this:

synchronized(a){
    synchronized(b){

.. in some other class:

synchronized(b) {
    synchronized(a) {

There we see an explicit pair of synchronizers and the out-of-order acquisition of locks in this case is a prime case for classic deadlock.

But what about this:

synchronized(a){
    synchronized(b){

.. in some other class:

synchronized(b){
    someMethodCall();
}

void someMethodCall() {
   synchronized(a) {

This case is slightly more tricky:  the locks are indeed obtained out of order, but not immediately.  In fact the acquisition of the second lock on ‘a’ might not occur in the next method; it might occur several stack frames later.  You might not find out about it unless you use a thread-analyser package (very pricey), or a ticket hits your mailbox (and deadlocks generally count as Critical/Escalated priority).

The acquisition of the second lock isn’t obvious.   The signature of the method doesn’t make it obvious;  the only way you’ll know is by looking at the code, and if there are many execution branches away from this point in the code, that is going to take an unfeasibly long time.

The javadoc for the method might make it clear in the contract for the method, but again:  if there are still many possible method calls after this one, that’s a lot of javadoc to review.

So how do we solve this problem?

Don’t obtain the locks at all.  If you don’t need them, don’t grab them.  No locking means no chance of deadlock.  The corollary of this rule is that the fewer locks you grab, the lower the chance of deadlock.

Keep the scope of locks as small as possible.  The shorter time you hold a lock for, the lower the chance of deadlock

Obtain the locks in the same order, everywhere.  In extreme cases, this might mean obtaining a lock twice.  To go back to our second example above:

synchronized(a){
    synchronized(b){

.. in some other class:

synchronized(a) {
    synchronized(b){
        someMethodCall();
}

void someMethodCall() {
   synchronized(a) {

Of course, if you could eliminate one or both synchronizers, that would be the best solution, but this one also works because Java’s locks are re-entrant:  the second acquisition in someMethodCall effectively does nothing, since the thread already holds the monitor thanks to the explicit acquisition prior to the call.  However, it does make the locking order a -> b, instead of b -> a, eliminating the deadlock.

It’s not possible in some cases to eliminate synchronizers.  Time pressures may mean the test set for adding a synchronizer is much smaller than that for removing one and therefore preferable;  you may not be able to influence the code in someMethodCall for whatever reason, etc. etc.

Being aware of the effects and dangers of synchronization when you write code should help you avoid the pitfalls.  Synchronizers are useful and sometimes necessary, but with great power comes great responsibility.

I think I heard that in a movie somewhere.

  • Share/Bookmark
  1. February 21st, 2009 at 14:09 | #1

    Very good and very true. I know about this stuff but its just amazing how often you forget it when coding and then have the dreaded task of debugging the application to see what’s going wrong. Think about it early and it will save headaches later.

    Synchronization is one of those things that when it works is absolutely amazing but, you have a greater chance of something going tits up when you are not experienced. Long live Erlang :)

  2. February 23rd, 2009 at 07:56 | #2

    Exactly, and they can be very useful if you’re doing Object.Wait()/.Notify() semaphores (which bring their own special class of problems). Debugging them ranges from impossible (if they’re suspected) to hard (if you have a stack trace of one actually happening). At least in the newer JVMs the deadlock detector will actually point out the participants in a deadlock right there in the trace.

  1. No trackbacks yet.