23.4 Writing Thread-Safe Code

Thread-safety is a critical property of a concurrent application. Threads can generally execute their tasks concurrently without impending each other, unless they are sharing resources. Generally speaking, a shared resource can be any data that is visible to more than one thread. This could be an object or a primitive value, which multiple threads can access concurrently.

Each thread has its own execution stack that contains local variables of active methods during execution (§22.2, p. 1369). Any variable allocated on this stack is only visible to the thread that owns this stack, and is therefore automatically thread-safe. On the other hand, objects are placed in a heap which is shared by all threads. These objects are considered to be potentially visible to all threads. A thread can access any object in a heap if it has a reference to it. The challenge is to avoid thread interference (or race conditions) when concurrent threads are accessing a shared object.

As a running example, we will use different implementations of a counter to illustrate various approaches to achieving thread-safety. Example 23.7 shows the interface ICounter at (1) that is implemented by a counter. The interface defines the two methods increment() and getValue() to increment and read the value in a counter, respectively.

The class TestCounters at (3) tests various counter implementations from (5) to (10) in the main() method at (4) by calling the method runIncrementor() defined at (11). This method creates a Runnable incrementor at (12) that calls the increment() method of a counter a fixed number of times (NUM_OF_INCREMENTS). The incrementor is submitted a fixed number of times to an executor service in the try block at (13), corresponding to the size of the thread pool of the executor service (POOL_SIZE). All together, a counter is incremented 10,000 times (NUM_OF_INCREMENTS*POOL_SIZE).

The output shown in Example 23.7 is for the various counter implementations that will be discussed in this section. First up is an unsafe counter implementation at (2) which is not thread-safe, as we can see from the incorrect result in the output. The cause can be attributed to thread interference (§22.4, p. 1388) and memory consistency errors (§22.5, p. 1414) when accessing the shared counter field by the concurrent threads. We look at different solutions (in addition to the synchronized code) to implement mutually exclusive access of a shared object in a concurrent application.

Example 23.7 Testing Counter Implementations

Click here to view code image

package safe;
/** Interface that defines a basic counter. */
interface ICounter {                                                      // (1)
  void increment();
  int getValue();
}

Click here to view code image
package safe;
public class UnsafeCounter implements ICounter {                          // (2)
  private int counter = 0;
  @Override public int getValue()   { return counter; }
  @Override public void increment() { ++counter; }
}

Click here to view code image

package safe;
import java.util.concurrent.*;
import java.util.stream.IntStream;
public class TestCounters {                                               // (3)
  private static final int NUM_OF_INCREMENTS = 1000;
  private static final int POOL_SIZE = 10;
  public static void main(String[] args) throws InterruptedException {    // (4)
    UnsafeCounter usc = new UnsafeCounter();                              // (5)
    runIncrementor(usc);
    System.out.printf(“Unsafe Counter: %24d%n”, usc.getValue());
    VolatileCounter vc = new VolatileCounter();                           // (6)
    runIncrementor(vc);
    System.out.printf(“Volatile Counter: %22d%n”, vc.getValue());
    SynchronizedCounter sc = new SynchronizedCounter();                   // (7)
    runIncrementor(sc);
    System.out.printf(“Synchronized Counter: %18d%n”, sc.getValue());
    AtomicCounter ac = new AtomicCounter();                               // (8)
    runIncrementor(ac);
    System.out.printf(“Atomic Counter: %24d%n”, ac.getValue());
    ReentrantLockCounter rlc = new ReentrantLockCounter();                // (9)
    runIncrementor(rlc);
    System.out.printf(“Reentrant Lock Counter: %16d%n”, rlc.getValue());
    ReentrantRWLockCounter rwlc = new ReentrantRWLockCounter();           // (10)
    runIncrementor(rwlc);
    System.out.printf(“Reentrant Read-Write Lock Counter: %d%n”, rwlc.getValue());
  }
  public static void runIncrementor(ICounter counter) {                   // (11)
    // A Runnable incrementor to call the increment() method of the counter
    // a fixed number of times:
    Runnable incrementor = () -> {                                        // (12)
      IntStream.rangeClosed(1, NUM_OF_INCREMENTS).forEach(i->counter.increment());
    };
    // An executor service to manage a fixed number of incrementors:
    ExecutorService execService = Executors.newFixedThreadPool(POOL_SIZE);
    // Submit the incrementor to the executor service. Each thread executes
    // the same incrementor, and thereby increments the same counter.
    try {                                                                 // (13)
      IntStream.range(0, POOL_SIZE).forEach(i -> execService.submit(incrementor));
    } finally {
      execService.shutdown();
    }
    // Wait for all tasks to complete.
    try {
      while (!execService.awaitTermination(1, TimeUnit.SECONDS));
    } catch (InterruptedException e) {
      execService.shutdownNow();
    }
  }
}

Probable output from the program:

Click here to view code image Unsafe Counter:                     8495
Volatile Counter:                   8765
Synchronized Counter:              10000
Atomic Counter:                    10000
Reentrant Lock Counter:            10000
Reentrant Read-Write Lock Counter: 10000