Click here to Skip to main content
15,860,859 members
Articles / Programming Languages / Java / Java SE / J2SE 5.0

4 Simple Rules for Effective Bug-free Concurrent Programming

Rate me:
Please Sign up or sign in to vote.
4.27/5 (8 votes)
27 Feb 2009Apache5 min read 33K   19   3
This article shows with an example some rules for effective concurrent programming

Introduction 

Writing correct programs is hard, but writing correct concurrent programs is far harder. Despite that, a Java developer (at any competence level) can't just ignore the problem. And in the very last year, there’s another good reason to take care even more of multithreading programming: the race towards always higher CPU frequency and number of transistors integrated on a single core is going to finish because the CPU makers are starting to face some physical limitations with the request of less power demanding devices. Today it is already possible to find lots of (even low-end) devices having two or more cores.

For these reason, programs are moving from being concurrent, serving more requests at the same time, to being parallel, having the possibility to split the execution of a complex time-consuming request in more subtasks that can run together in order to take advantage of multi-processor or multi-core systems. This scenario brings new (stimulating) challenges on the desktop of each Java developer and each one of us should try to acquire the necessary skills to win these challenges.

The Base Example

The purpose of this article is to mention and illustrate some simple but very important rules for writing effective and bug-free Java multithreaded programs. The rationale behind those rules will be justified by analyzing and improving this very simple Java class:

Java
public class FactorialCalculator {
    public BigInteger factorialOf(BigInteger i) {
        BigInteger result = calculateFactorial(i);
        return result;
    }

    private BigInteger calculateFactorial(BigInteger i) {
        int compare = i.compareTo(BigInteger.ONE);
        if (compare < 0) throw new IllegalArgumentException();
        else if (compare == 0) return i;
        BigInteger i1 = i.subtract(BigInteger.ONE);
        return i.multiply(calculateFactorial(i1));
    }
}

Stateless is Thread-safe 

This class has only a single public method that calculates the factorial of the given BigInteger parameter. What happens if we share an instance of this class through different threads? Is that instance thread-safe? Of course, the answer is yes and for a very simple reason: one thread accessing a FactorialCalculator cannot influence the result of another thread accessing the same object, because the two threads do not share state. This is probably the easiest to understand and at the same time most important rule of multithreaded programming: stateless objects are always thread-safe.

Modify State Consistently

Of course to have objects with some sort of state is often just unavoidable. In order to add a little state to the first example, let's make it just a bit more complicated by adding a variable that counts the number of times the factorialOf() method is called:

Java
public class FactorialCalculator {
    private long count;

    public BigInteger factorialOf(BigInteger i) {
        BigInteger result = calculateFactorial(i);
        ++count;
        return result;
    }
    public long getCount() { return count; }
}

What about thread-safety now? Since our FactorialCalculator is no longer a stateless object, we have to pay a bit more attention to what could happen during a multithreaded execution. Actually while the increment operation (++count) may look like a single action because of its compact syntax, it is not atomic, which means that it does not execute as a single, indivisible operation. Instead, it is a shorthand for a sequence of three discrete operations: fetch the current value, add one to it, and write the new value back. Even worse, in this example, we could not only read a stale value, but also a pseudo-random value that has not been written by any thread. That’s why the Java Memory Model requires fetch and store operations to be atomic, but for nonvolatile long and double variables, the JVM is permitted to treat a 64-bit read or write as two separate 32-bit operations. These considerations make it evident that our object is now susceptible to what is called a lost update and that its thread-safety is definitively broken. This problem put in evidence the second simple rule for a bug-free multithreaded program: always check for atomicity.

Know Your Library

To fix the lack of atomicity problem in our simple code is quite easy. Starting from Java 5.0, the java.util.concurrent.atomic package contains atomic variable classes for effecting atomic state transitions on numbers and object references. By replacing the long counter with an AtomicLong, we ensure that all actions that access the counter state are atomic:

Java
public class FactorialCalculator {
    private final AtomicLong count = new AtomicLong(0);

    public BigInteger factorialOf(BigInteger i) {
        BigInteger result = calculateFactorial(i);
        count.incrementAndGet();
        return result;
    }

    public long getCount() { return count.get(); }
}

Atomic variable classes are just one of the many improvements offered by Java 5 over the previous versions that allow to make concurrent development easier. Other fundamental features provided by Java 5 are a new set of thread-safe collections, an executor framework for running unrelated tasks and several other kinds of classes that have been added to better support advanced concurrent design like Semaphore, ReentrantLock and CountDownLatch. A good knowledge of these classes and their APIs allow to build our concurrent programs on a set of well-proven thread-safe libraries without the need to reinvent the wheel.

Preserve Invariants 

Now suppose we want to improve the FactorialCalculator even a bit more. In particular, we discovered that for some reason the clients of our object tend to ask the factorial of a given number more than once quite often, so we decided to improve its performance by caching the last computed factorial value as it follows:

public class FactorialCalculator {
    private final AtomicReference<BigInteger> lastNumber = 
				new AtomicReference<BigInteger>();
    private final AtomicReference<BigInteger> lastFactorial = 
				new AtomicReference<BigInteger>();

    public BigInteger factorialOf(BigInteger i) {
        if (i.equals(lastNumber.get())) 
            return lastFactorial.get();
        BigInteger result = calculateFactorial(i);
        lastNumber.set(i);
        lastFactorial.set(result);
        return result;
    }
}

We update the values of lastNumber and lastFactorial via AtomicReferences so the object’s thread safety is still guaranteed, isn't it? Unfortunately no. This approach does not work, because even though the atomic references are individually thread-safe, FactorialCalculator still has a race condition that could make it produce the wrong answer.

An invariant is a condition that, in order to preserve the correctness of a multi-threaded program, needs to be verified immediately before and after each atomic operation performed by the program itself. The only invariant of FactorialCalculator is that the factorial value cached in lastFactorial has to correspond to the value cached in lastNumber and it is correct only if this invariant always holds. What is said implies that the third rule to keep bugs out of our parallel programs is: ensure that invariants are preserved regardless of timing or interleaving of operations in multiple threads.

Use Small Synchronized Blocks

As stated, in order to preserve the FactorialCalculator invariant, we need to keep synchronized the code that modifies its state as it follows:

Java
public class FactorialCalculator {
    private final AtomicReference<BigInteger> lastNumber = 
				new AtomicReference<BigInteger>();
    private final AtomicReference<BigInteger> lastFactorial = 
				new AtomicReference<BigInteger>();

    public BigInteger factorialOf(BigInteger i) {
        synchronized(this) {
            if (i.equals(lastNumber.get())) 
                return lastFactorial.get();
        }
        BigInteger result = calculateFactorial(i);
        synchronized(this) {
            lastNumber.set(i);
            lastFactorial.set(result);
        }
        return result;
    }
}

This last version of our class enforces the invariant of the last cached result by guarding the read and write operations on lastNumber and lastFactorial with the same lock. Note that we could achieve the same result in an easier (and more readable way) by synchronizing the whole factorialOf() method, but this poor choice implies that all the invocations to that method will be completely serialized, making useless our effort to write a thread safe class that can perform well in a multithreaded environment. From this last consideration derives the fourth rule: keep synchronized blocks as small as possible (but not smaller) in order to not excessively compromise the parallel execution of our code.

History

  • 28th February, 2009: Initial post

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0


Written By
Switzerland Switzerland
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionHow do small synchronized blocks affect performance? Pin
HosamAly4-Mar-09 7:23
HosamAly4-Mar-09 7:23 
AnswerRe: How do small synchronized blocks affect performance? Pin
Mario Fusco4-Mar-09 9:44
Mario Fusco4-Mar-09 9:44 
AnswerRe: How do small synchronized blocks affect performance? Pin
KevinAG5-Mar-09 10:44
KevinAG5-Mar-09 10:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.