Untangling Java concurrency

Modern threading: A Java concurrency primer

Understanding Java threads in a post-JDK 1.4 world

1 2 Page 2
Page 2 of 2

Listing 3. MonitorModel

import java.util.Random;

class MonitorModel {
   public static void main (String [] args) {
      new RemoteHost("example 1");
      new RemoteHost("example 2");
   }
}

class MonitoredObject {
}

// This models a simple monitor of a remote host, as an
// administrative program might use.  A new value from
// the RemoteHost appears every 'delay' milliseconds, and
// is reported to the screen.
class RemoteHost extends MonitoredObject implements Runnable {
    String name;
    int delay;
    RemoteHost(String n) {
        name = n;
        new Thread(this).start();
    }
    public void run() {
      int count = 0;
      Random r = new Random();

        // Continue indefinitely, until a keyboard interrupt.
      while (count++) {
         try {
            int delay = r.nextInt(1000);
            System.out.format(
              "Line #%d from RemoteHost '%s', after %d-milliseconds.\n",
                                             count, name, delay);
            Thread.currentThread().sleep(delay);
         }
         catch (InterruptedException ie) {
            // This would be a surprise.
         }
     count++;
      }
    }
}

When you run this example, you'll see output from two different threads interleave unpredictably, as is likely to happen in a real-world system monitor. Output is likely to include "runs" where one RemoteHost reports repeatedly while the other is silent. Listing 4 shows three notices from 'example-1' in succession:

Listing 4. Output from MonitorModel

.
      .
      .
Line #54 from RemoteHost 'example 1', after 875-milliseconds.
Line #59 from RemoteHost 'example 2', after 964-milliseconds.
Line #55 from RemoteHost 'example 1', after 18-milliseconds.
Line #56 from RemoteHost 'example 1', after 261-milliseconds.
Line #57 from RemoteHost 'example 1', after 820-milliseconds.
Line #60 from RemoteHost 'example 2', after 807-milliseconds.
Line #58 from RemoteHost 'example 1', after 525-milliseconds.
      .
      .
      .

Using Runnable is thus nearly as succinct as directly subclassing from Thread. (Remember that most application code relies on Runnable definitions rather than Thread subclassing, if only to avoid a multiple-inheritance conflict.)

About monitors

Note that monitor in Listing 3 stems from the vocabulary of system administrators or network operators, who monitor objects -- often physical ones -- for which they're responsible. Coincidentally, programming theory applies the same word to a specific concept in concurrency. In a classic JavaWorld article from 1997, Bill Venners explained monitors thus:

A monitor is basically a guardian in that it watches over a sequence of code, making sure that only one thread at a time executes the code ... Each monitor is associated with an object reference. When a thread arrives at the first instruction in a block of code that is under the watchful eye of a monitor, the thread must obtain a lock on the referenced object. The thread is not allowed to execute the code until it obtains the lock. Once it has obtained the lock, the thread enters the block of protected code.

The examples of multithreading to this point have involved little coordination between threads; there's been no particular consequence for one thread executing before another, nor much teamwork required between them. A programming monitor is one of several mechanisms for managing thread synchronization. Locking is another concept, which I'll discuss shortly.

 

When good threads go bad ... good programmers make them better

There are two ways of constructing a software design. One way is to make it so simple that there are obviously no deficiencies. And the other way is to make it so complicated that there are no obvious deficiencies. -- C.A.R. Hoare

Hoare's words about the complications of coding seems to apply with special force to multithreading, with the result that many programmers simply shun thread-based code. Most of us can't do without the responsiveness and performance improvements of concurrency, however, especially as software systems evolve onto multicore architectures.

There's no getting around the reality that multithreading is hard. Worse, you might have heard it said that threads are a bad idea, meaning, prone to difficulty.

Some Java developers try to mitigate the hazards of thread programming by trading it in for newer (or just different) styles. Message-passing models expressed in terms of actors and agents eliminate many of the difficulties of shared state and synchronization. So, in a different way, do closures, which come baked into Java 7. The popularity of these alternative concurrency models in Java-related languages like Clojure has amplified their familiarity in the Java community proper.

Still, you might prefer to work with the standard Java threading architecture. As your applications grow more sophisticated, you'll need to learn not only how to launch runnables, but also to synchronize thread execution, communicate between threads, and manage thread lifetimes. With a little effort and the well-established design techniques discussed below you can make your multithreaded code as understandable and reliable as any other Java source.

Loose coupling

If you are going to stick with the Java Threads API and java.util.concurrent, what can you do to reduce the hazards of multithreading? First, develop your own sense of functional style. You know to minimize use of goto and global variables; in much the same way, design your Runnables to be as simple and loosely coupled as possible. "Loose coupling" here has several aspects:

  • Share as little state as possible
  • Minimize side-effects and emphasize immutable objects
  • Use standard design patterns (like Subscribe-Publish) to manage shared resources
  • Minimize inter-thread coordination
  • Simplify the lifespans of threads

Write testable code

Make your threads testable. A conventional application might build in facilities to ensure not just code coverage, but the precise results of "edge cases": does the program behave correctly when a limit is reached? When within one unit of the limit? In the same way, construct your multithreaded application to exercise threads under reproducible, salient conditions: with only a single worker thread operating; with two threads, started in either order; with more threads than the test host has cores; with controllable simulated loads; and so on.

Debug your code

Next, learn how to debug multithreaded applications. Studying best practices for debugging will improve your code measurably. See Esther Schindler's "Learning and improving your debugging skills" for a collection of valuable tips and exercises that will improve your Java debugging techniques.

FInally, there are tools. In the early years of Java, standalone utilities to help with debugging of thread problems were popular; more recently, the leading debuggers have incorporated essentially all of this functionality. (For instance, nearly every debugger, including jdb, supports introspection on threads.) Know what your favorite debugger offers and be sure that it compares well with the alternatives.

 

What's new in Java 6 and 7

The basics of multithreading have changed remarkably little since the early days of the Java platform. One fortunate change, however, is that multithreading has become easier in several important regards.

First, when Java 6 was released at the end of 2006 it brought considerable attention to locking. Locking is an important technique because threads sometimes contend for resources: two different threads might need to update a global variable that tracks how many users are active, or to update a bank account balance. It's very bad, though, to allow the threads to interfere with each other. In the case of an accounting program, imagine a starting balance of $100. One thread tries to debit $10, the other to credit $20. Depending on the exact sequence of operations, it's easy to end up with a final balance of $120 or $90 rather than the correct combination of $110.

Software applications need to guarantee correct results. The most common way to achieve this is with locks or other means of synchronizing the access of different threads. One thread -- the credit one, say, in the example of the last paragraph -- locks the account balance, completes its operation, sets the balance to $120. At that point, it releases the lock to the debit thread, which itself locks the balance, until it has successfully updated the balance to the correct sum of $110.

Perils of locking

Locks have the reputation of being difficult and costly, and occasionally they're simply wrong. Improvements to Java 6 and the JVMs that implement locks improved performance and refined programmatic control over them. The result: the speed of multithreaded applications generally improved, sometimes dramatically. (See "Do Java 6 threading optimizations actually work?" for more about threading optimization in Java 6.)

With the release of Java 7 in the summer of 2011, we saw a different kind of improvement to thread programming: removal! One common application of multithreading has been to answer questions such as, "is there a new file in this FTP repository?" or "have any results been appended to this logfile lately?" Java 7 includes a filesystem monitor and support for asynchronous input/output in java.nio.file. So the nio API allows us to directly code file update functionality, thus eliminating one need for programmers to write multithreaded source.

Closures also reduced the pressure to code with threads. For instance, the java.util.concurrent package includes support for writing concurrent loops (Neal Gafter, 2006).

 

Conclusion: What's next for threads

Current public plans for Java 8 and Java 9 appear to have little direct impact on threaded programming. A major goal of Java 8, scheduled for mid-2013, is more effective use of multicore environments. Expansion of java.util.concurrent.Callable closures will encourage their use (under the formal language label of "lambda") as an alternative to multithreading.

One of the goals for Java 9 is "massive multicore" compatibility. It's not yet clear how this will be effected at the level of language syntax.

One of the most interesting conclusions of the last fifteen years of Java concurrency programming is that threads are not so much to be avoided as handled with respect. Some teams do best with new java.util.concurrent techniques, or even some of the alternative models I've mentioned. For many teams, though, the best results come from using threads, but using them with the wisdom of the last decade. This generally involves simple synchronization models, an emphasis on testability, small and understandable class definitions, and teams that carefully review each other's source.

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

Threads programming

Thread synchronization

Locks and monitors

Testing and debugging threads

Threads & alternatives

1 2 Page 2
Page 2 of 2