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.

Inter-thread communication: java.util.concurrent.locks

Introductory thread communication is typically like what you saw in the RockScissorsPaper program: it travels back to (and from) the parent process. In some situations, reliable, direct communication between threads is also desirable. (For instance, many engineering and network calculations involve neighboring components, often modelled by threads deterministically exchanging data with a small number of nearby threads.)

Superficially, the simplest way for one thread to send a signal or reliable communication to another thread is called busy waiting. In busy waiting, implemented by wait/notify, the sending thread emits the signal as soon as it can, while the receiving thread blocks until the signal arrives, perhaps by way of a variable within a synchronized segment.

Busy waiting is a species of polling -- with all of the associated inefficiencies. (See "Five ways to maximize Java NIO and NIO.2" for my recent discussion about Java I/O and polling.)

More about wait/notify

See "Understanding Java threads, Part 3: Thread scheduling and wait/notify" to learn more about problems associated with using wait()/notify() to manage thread priority.

These days wait/notify has been mostly replaced by java.util.concurrent.locks, which I strongly recommend, especially because thewait/notify construct is often fragile. For instance, code with wait/notify usually doesn't account for spurious wake-ups, making it subject to sporadic errors. java.util.concurrent.locks is not only easier to get right, it's more flexible and performs at least as well. Other (and older) well-designed, high-quality concurrency constructs for the Java platform include semaphores and atomic variables.

Listing 5 is an example of inter-thread communication using java.util.concurrent.locks.

Listing 5. Inter-thread communication with java.util.concurrent.locks

import java.util.concurrent.locks.*;
import java.util.*;
import java.text.*;

public class lock_example {
    public static Lock this_lock = new ReentrantLock();
    public static void main(String[] args) throws Exception {
    ListenerThread lt = new ListenerThread();
        SpeakerThread st = new SpeakerThread();
    lt.start();
        st.start();
    }
    public static int message = 0;
    public static void report(String label) {
        Date now = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("hh:mm:ss.S");
        String time_of_day = sdf.format(now.getTime());
    System.out.format("%s:  %s\n", time_of_day, label);
    }
}

class ListenerThread extends Thread {
    public void run() {
    Thread this_thread = Thread.currentThread();
        while (true) {
            lock_example.report("The Listener requests the lock.");
            lock_example.this_lock.lock();
            lock_example.report("The Listener acquires the lock.");
            if (lock_example.message == 0) {
                lock_example.report("The Speaker has sent nothing.");
                lock_example.report("The Listener will check again later.");
                try {
                    this_thread.sleep(800);
                } catch (InterruptedException ie) {
                    // ie
                }
            } else {
                lock_example.report("The Listener just received " + Integer.toString(lock_example.message) + " from the Speaker.");
                lock_example.message = 0;
            }
            lock_example.report("The Listener releases the lock.");
            lock_example.this_lock.unlock();
        }
    }
}

class SpeakerThread extends Thread {
    int counter = 0;
    public void run() {
    Thread this_thread = Thread.currentThread();
        while (true) {
            while (0 != lock_example.message) {
                try {
                    lock_example.report("The Speaker waits for the Listener to catch up.");
                    this_thread.sleep(30);
                } catch (InterruptedException ie) {
                }
            }
            counter++;
            lock_example.report("The Speaker requests the lock.");
            lock_example.this_lock.lock();
            lock_example.report("The Speaker acquires the lock.");
            lock_example.report("The Speaker takes a nap--a model for real work it might otherwise do.");
            try {
                this_thread.sleep((int) (2000 * Math.random()));
            } catch (InterruptedException ie) {
            }
            lock_example.message = counter;
            lock_example.report("The Speaker releases the lock.");
            lock_example.this_lock.unlock();
        }
    }
}

Listing 6 shows the output from a run of this program.

Listing 6. Output of lock_example

07:55:38.545:  The Speaker requests the lock.
07:55:38.572:  The Speaker acquires the lock.
07:55:38.573:  The Speaker takes a nap--a model for real work it might otherwise do.
07:55:38.541:  The Listener requests the lock.
07:55:39.876:  The Speaker releases the lock.
07:55:39.877:  The Listener acquires the lock.
07:55:39.878:  The Listener just received 1 from the Speaker.
07:55:39.879:  The Listener releases the lock.
07:55:39.880:  The Listener requests the lock.
07:55:39.881:  The Listener acquires the lock.
07:55:39.881:  The Speaker has sent nothing.
07:55:39.883:  The Speaker requests the lock.
07:55:39.882:  The Listener will check again later.
07:55:40.684:  The Listener releases the lock.
07:55:40.685:  The Speaker acquires the lock.
07:55:40.685:  The Speaker takes a nap--a model for real work it might otherwise do.
07:55:40.687:  The Listener requests the lock.
07:55:41.330:  The Speaker releases the lock.
07:55:41.331:  The Speaker waits for the Listener to catch up.
07:55:41.332:  The Listener acquires the lock.
07:55:41.333:  The Listener just received 2 from the Speaker.
07:55:41.335:  The Listener releases the lock.
07:55:41.335:  The Listener requests the lock.
07:55:41.336:  The Listener acquires the lock.
07:55:41.337:  The Speaker has sent nothing.
07:55:41.339:  The Listener will check again later.
07:55:41.362:  The Speaker requests the lock.
07:55:42.140:  The Listener releases the lock.
07:55:42.142:  The Speaker acquires the lock.
07:55:42.142:  The Speaker takes a nap--a model for real work it might otherwise do.
07:55:42.143:  The Listener requests the lock.
07:55:43.606:  The Speaker releases the lock.
07:55:43.608:  The Speaker waits for the Listener to catch up.
07:55:43.608:  The Listener acquires the lock.
07:55:43.611:  The Listener just received 3 from the Speaker.
07:55:43.612:  The Listener releases the lock.
07:55:43.613:  The Listener requests the lock.
07:55:43.614:  The Listener acquires the lock.
07:55:43.615:  The Speaker has sent nothing.
07:55:43.615:  The Listener will check again later.
07:55:43.638:  The Speaker requests the lock.
07:55:44.416:  The Listener releases the lock.
07:55:44.417:  The Speaker acquires the lock.
07:55:44.417:  The Speaker takes a nap--a model for real work it might otherwise do.
07:55:44.417:  The Listener requests the lock.
07:55:45.433:  The Speaker releases the lock.
07:55:45.434:  The Speaker waits for the Listener to catch up.
07:55:45.435:  The Listener acquires the lock.
07:55:45.436:  The Listener just received 4 from the Speaker.
07:55:45.437:  The Listener releases the lock.
  ...

Note that ReentrantLock() permits the Speaker to deliver its message reliably, without the risk of overwriting or the inefficiencies of polling. As bulky as this source code is, it's significantly more compact than most correctly implemented locking programs that use more primitive threading constructs.

The dreaded deadlock

Whereas we've been talking so far about programming constructs to use, a deadlock is something to avoid. In fact, some say deadlock is an anti-pattern.

Deadlock anti-patterns

See Obi Ezechukwu's three-part series introducing deadlock anti-patterns:

A deadlock occurs when more than one actor is waiting on another actor in a cycle that has no exit. For instance, suppose one thread were waiting on account.balance to be freed so that it could finish its computation of account.available_for_investment. Then, suppose that the thread holding account.balance were waiting for account.available_for_investment in order to compute account.balance. In a situation where both threads are stuck waiting indefinitely, we have what's known as a deadlock.

Deadlock analysis is one of the fundamental, unavoidable challenges of multithreaded programming. Specialized "best practices" developed in this area include calculation of a hierarchy of critical sections. The next section introduces some lightweight techniques to avoid deadlock.

Executors and thread pools

You might have noticed that the RockScissorsPaper example at the beginning of this article relies on a thread pool. A thread pool (sometimes called a worker thread crew or a replicated thread worker collection ) is a team of threads managed in coordination. Thread pools are often implemented to receive assignments of pending tasks. Management at the pool level has at least the potential to improve performance by eliminating the cost of creating and cleaning up one thread for each task.

The RockScissorsPaper program, for instance, could have used distinct new Thread() instances, and start()ed each one separately. Instead, I wrote the program to submit() them to a thread pool. The thread pool was then responsible for ensuring that each thread was properly scheduled for execution.

Thread pools in java.util.concurrent

See "Hyper threaded Java: Using the Java concurrency API for time-consuming tasks" for an in-depth discussion of thread pools and the ThreadPoolExecutor utility class. Then follow up with "Hanging thread detection and handling," which introduces a custom thread pool with built-in hanging detection.

Thread pooling is a common and very useful practice among languages that manage threads, and Java does well to make it available in the standard library. Thread pools are commonest when the number of tasks assigned to the pool is larger than the number of threads available to run those tasks. Wisely managing threads can greatly accelerate task completion in such a scenario.

Brian Goetz, et al., discussed thread pools and the Executor interface in Java Concurrency in Practice (Addison-Wesley, 2006). See the chapter excerpt on executing tasks in threads to learn more about the theory behind the RockScissorsPaper program in Listing 1.

In conclusion

The biggest problem with multithreaded programming is that it's so commonplace. Having programmed with Threads once or twice, some new developers will conclude that they understand Java concurrency.

In fact, multithreading is a deep and broad subject, far more so than any one program -- or even a handful of them -- could capture. In this article I've presented only a fraction of the concurrency constructs that are standard to the Java platform, both pre- and post-java.util.concurrent. Moreover, my examples don't exhaust the capabilities of thread pools, locks, and so on: nearly all the specific method invocations above assume several default values. As you become more expert with threading, you'll encounter real-world requirements that make it to your advantage to use the concurrent application programming interface (API) in more sophisticated ways. You'll refine your method calls with non-default arguments, and you'll learn more advanced method calls, to match the specific needs of your programs.

Getting good at threaded programming is an investment worthy of at least 10,000 hours. Let the material here serve as a starting point for your experimentation.

Cameron Laird began writing Java code before it was called Java and has contributed occasionally to JavaWorld in the years since then. Keep up with his coding and writing through Twitter as @Phaseit.

Learn more about this topic

Learn more about topics related to multithreaded programming and concurrency on the Java platform.

java.util.concurrent

Callable and Runnable

Wait/notify

Thread safety

Join the discussion
Be the first to comment on this article. Our Commenting Policies