Untangling Java concurrency

Modern threading for not-quite-beginners

Best practices to avoid Java concurrency pitfalls

1 2 Page 2
Page 2 of 2

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

1 2 Page 2
Page 2 of 2