Modern threading for not-quite-beginners

Best practices to avoid Java concurrency pitfalls

Cameron Laird revisits the practice and concepts of multithreaded programming in Java, this time focusing on more intermediate programming solutions for today's distributed computing problems. Build on what you know about the java.util.concurrent package while learning techniques to improve inter-thread communication and avoid Java concurrency pitfalls.

Multithreaded programming in Java has a reputation for difficulty, but most developers can untangle it with smart, designed-for-concurrency constructs that are standard with the Java platform. In this follow-up to my survey of basic modern threading techniques, I'll introduce some of the constructs in Doug Lea's java.util.concurrent package and also discuss a few standbys of Java threading horror -- which aren't actually such a big deal when properly worked around. All in all, I will touch on seven topics that can help you make the best, or the worst, of your multithreaded programs:

Note that some of the examples in this article build on my discussion in "Modern threading: A Java concurrency primer" (JavaWorld, June 2012).

Thread management (a recurring theme)

Programming with Java threads isn't really so hard -- it's thread management that keeps most software developers up at night. Consider the analogy of painting a room: more of your time will be spent on preparation than execution -- choosing and matching colors, clearing and taping the room, and so on. This is because today's paints and brushes make the painting part about as simple and "goofproof" as can be. Setup, as it turns out, is more than two-thirds of the game.

Thread programming works similarly: Using threads is generally much easier than managing (or cultivating) them over the long term. Thread management will be a recurring theme in your study of thread programming, so you might as well start thinking about it now.

For instance, in my previous article I introduced thread management as a simple evaluation of new ExampleThread(). That thread was intended to be destroyed at the end of scope, which is fine for a simple program. But now we're ready to dig into some more sophisticated schemes. In the next sections, look for programs that do some of the following:

  • Delete or re-use Thread instances
  • Manage different varieties of a Thread
  • Require introspection on Thread characteristics such as memory use or life history

Runnable vs Callable

In my last article I introduced a MonitorModel based on a Runnable rather than a Thread. Runnable's more flexible inheritance model gives it the advantage over Thread. On the other hand, both Runnable and Thread share certain limits: neither returns values or throws Exceptions.

For even more capable thread programming, go beyond both Thread and Runnable to use Callable. Callable communicates better than its two friends, because Callable returns results.

Callable is part of the java.util.concurrent package, which first appeared in the Java 5 end-of-summer 2004 release. The program in Listing 1 illustrates both the flexibility and the complications of using Callable:

Listing 1. RockScissorsPaper with Callable

import java.util.concurrent.*;

public class RockScissorsPaper {
    public static class PlayerCallable implements Callable {
        String name;
        int call_sequence = 0;
        static String[] SelectionTable = {
            "Rock", "Scissors", "Paper"
        };
        PlayerCallable(String given_name) {
            name = given_name;
        }
        public String call() throws InterruptedException {
            int delay = (int) (2000 * Math.random());
            call_sequence++;
            System.out.format("%s pauses %d microseconds on the %d-th invocation.\n", name, delay, call_sequence);
            Thread.sleep(delay);
            String choice = SelectionTable[three_sided_coin()];
            System.out.format("%s selects %s.\n", name, choice);
            return choice;
        }
    }
    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        PlayerCallable player1 = new PlayerCallable("player1");
        PlayerCallable player2 = new PlayerCallable("player2");
        for (int i = 10; i > 0; i--) {
            Future future1 = pool.submit(player1);
            Future future2 = pool.submit(player2);
            System.out.println(payoff((String) future1.get(),
                                      (String) future2.get()));
        }
        pool.shutdown();
    }
    public void run() {
        FutureTask player1 = new FutureTask(new ThisCallable());
    }
    public static int three_sided_coin() {
    return (int)(Math.random() * 3);
    }
    public static String payoff (String first_hand, String second_hand) {
    if (first_hand.equals(second_hand)) {
        return String.format("'%s' from both hands is a tie.", 
              first_hand);
    }
        if ((first_hand.equals("Rock") & second_hand.equals("Scissors")) ||
            (first_hand.equals("Scissors") & second_hand.equals("Paper")) ||
            (first_hand.equals("Paper") & second_hand.equals("Rock"))) {
            return String.format("One's '%s' beats Two's '%s'.", first_hand, second_hand);
        }
        return String.format("Two's '%s' beats One's '%s'.", second_hand, first_hand);
    }
    public class ThisCallable implements Callable {
        public Integer call() throws java.io.IOException {
           return 1;
        }
    }
}

In this game of rock-scissors-paper, two players report to an umpire.Output from a typical run of this program would be as follows:

Listing 2. Output from Callable RockScissorsPaper

player1 pauses 1350 microseconds on the 1-th invocation.
player2 pauses 581 microseconds on the 1-th invocation.
player2 selects Rock.
player1 selects Paper.
One's 'Paper' beats Two's 'Rock'.
player1 pauses 942 microseconds on the 2-th invocation.
player2 pauses 314 microseconds on the 2-th invocation.
player2 selects Rock.
player1 selects Rock.
'Rock' from both hands is a tie.
player2 pauses 292 microseconds on the 3-th invocation.
player1 pauses 703 microseconds on the 3-th invocation.
player2 selects Paper.
player1 selects Paper.
'Paper' from both hands is a tie.
player1 pauses 1261 microseconds on the 4-th invocation.
player2 pauses 354 microseconds on the 4-th invocation.
player2 selects Scissors.
player1 selects Paper.
Two's 'Scissors' beats One's 'Paper'.
player1 pauses 1534 microseconds on the 5-th invocation.
player2 pauses 1860 microseconds on the 5-th invocation.
player1 selects Paper.
player2 selects Rock.
One's 'Paper' beats Two's 'Rock'.
player1 pauses 1025 microseconds on the 6-th invocation.
player2 pauses 906 microseconds on the 6-th invocation.
player2 selects Paper.
player1 selects Rock.
Two's 'Paper' beats One's 'Rock'.
player1 pauses 554 microseconds on the 7-th invocation.
player2 pauses 1975 microseconds on the 7-th invocation.
player1 selects Rock.
player2 selects Scissors.
One's 'Rock' beats Two's 'Scissors'.
player1 pauses 1175 microseconds on the 8-th invocation.
player2 pauses 774 microseconds on the 8-th invocation.
player2 selects Rock.
player1 selects Paper.
One's 'Paper' beats Two's 'Rock'.
player1 pauses 134 microseconds on the 9-th invocation.
player2 pauses 708 microseconds on the 9-th invocation.
player1 selects Scissors.
player2 selects Scissors.
'Scissors' from both hands is a tie.
player1 pauses 531 microseconds on the 10-th invocation.
player2 pauses 126 microseconds on the 10-th invocation.
player2 selects Scissors.
player1 selects Paper.
Two's 'Scissors' beats One's 'Paper'.

Some things to note about the program: First, the two players operate independently. Each waits a randomized time, from zero to two seconds, then chooses among Rock, Scissors, or Paper. Also note that the choices take place in separate threads, in indeterminate sequence. For example, in the tenth "hand" Player1 takes four times as long to choose, so Player2's choice appears on stdout first:

player1 pauses 531 microseconds on the 10-th invocation.
player2 pauses 126 microseconds on the 10-th invocation.
player2 selects Scissors.
player1 selects Paper.

Communication to and from each thread is fully programmable using the call() and get() methods. Invocation of the Callable completes immediately. Afterward, when the result of the calculation is ready, it appears through the Future mechanism. And finally, the ExecutorService assumes responsibility for assigning Callables to available Threads for execution. (More about that later.)

While using Callable involves more plumbing than using Runnable, it also makes for cleaner communication. Callable is generally a better choice than Runnable for use cases where computing threads need to exchange data with their invoking process. Runnable also might play rock-scissors-paper, but it would need a way to return the selection of Rock, Scissors, or Paper. An individual programmer would be hard-pressed to code such a communication more elegantly than Callable already does.

ExecutorService vs ForkJoinPool

ExecutorService was introduced in the java.util.concurrent package to help manage progress-tracking and termination for asynchronous tasks. Learn about ExecutorService (and its modernized sidekick, ForkJoinPool) in the Java tip, " When to use ExecutorService vs ForkJoinPool."

Shared resources and immutability

Multithreaded programming makes it much harder to reason about or understand code segments locally. That's because many resources have the potential to be shared between threads, so what happens in one code segment might depend on a distant source, executing in a different thread. The example in Listing 3 illustrates my point.

Listing 3. Example thread-hazardous code segment

...
    common.balance = getBalance();
    if (common.balance > common.threshold) {
      ...

If you're disturbed by what you see in Listing 3 then you are not alone! In a multithreaded context, common.balance might have a different value when tested than when it was assigned. While the two statements are consecutive in source code, during execution other source code in a different thread could intervene and update the common.balance value.

Worse, from a programmer's standpoint that sequence of execution isn't deterministic: it might vary from one run to the next.

An effective response to such difficulties is to program with immutable objects. For reasons that go beyond their use in threads, Joshua Bloch famously recommends that developers use immutable classes "unless there's a very good reason to make them mutable." For cases where a class cannot be immutable, he proposes that we limit the mutability "as much as possible" (see Effective Java in Resources).

Using immutable objects ensures thread safety. You can also attain thread safety by doing calculations on mutable objects whose only reference is within the local scope: if a thread can be guaranteed to have the only references to a resource, then using that resource is safe even if it's mutable.

More about immutability and thread safety

See Bill Venners's "Design for thread safety" for a quick tutorial on three ways to make an object thread safe. Vladimir Roubtsov's "Mutable or immutable?" defines and discusses immutable objects and patterns.

Synchronized blocks

Sometimes a calculation requires mutability, with references that can't be confined to a single thread; what to do then? This situation demands synchronization, which is a kind of locking that guarantees exclusive access by a thread to a shared resource.

Syntactically, the synchronized keyword can be applied to both methods and blocks. In broad terms, block synchronization is more useful. For instance, using block synchronization would transform the code sample from Listing 3 to the following:

Listing 4. Block synchronization enforces thread safety

...
    synchronized(common.balance) {
      common.balance = getBalance();
      if (common.balance > common.threshold) {
      ...
    }

The synchronized keyword locks the source code segment so that only one thread can execute at a time. You are thus guaranteed that common.balance will have the same value on reading within the thread as when it was written.

Alternately, you could use a slightly different syntax to lock resources for the span of an entire method:

...
    public static synchronized int getBalance() {
       ...

Synchronized locking guarantees that computation of getBalance() is atomic or transactional across its resources.

Synchronization is a relatively delicate matter: It applies only to blocks and methods, not variables. If mis-used it can result in pathologies like deadlock. Synchronization applies only to final fields, and it's managed by methods like wait() and notify(). You can also configure synchronization with java.util.concurrent.locks to yield interruptible or re-entrant locks, which I discuss in the next section. (Also see Resources.)

Avoid synchronization deadlocks

Brian Goetz's"Avoid synchronization deadlocks" is an in-depth look at how synchronized can lead to deadlock, followed by tips for working around it.

1 2 Page
Recommended
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more