Compound Mutually Exclusive Operations

A compound operation that requires multiple operations on a synchronized collection is not guaranteed to be thread-safe just because the individual operations are mutually exclusive. Concurrent threads executing such a compound operation can interleave the individual operations, as in the case of a compound arithmetic operator. Example 23.17 illustrates implementing a compound mutually exclusive operation on a synchronized list using coarse-grained synchronization.

The class DoubleAct has a synchronized list (names) declared at (1), and provides two methods add() and removeFirst() at (2) and (3) to maintain this list.

The Client class uses the DoubleAct class. An instance of the DoubleAct class is populated at (10) in the main() method. A Runnable (remover) is implemented by the lambda expression at (11). It calls the removeFirst() method at (12) and prints the retrieved name. Three threads are created at (13) that execute the Runnable remover, calling the removeFirst() method on the shared DoubleAct instance. Since there are only two elements in the list, two of the threads should print one name each, and one of them should return null.

The method removeFirst() at (3) uses two mutually exclusive methods of the synchronized list. The intent is to return the element at index 0 if the list is not empty; otherwise, the value null. The method size() returns the current size of the synchronized list, and the method remove() deletes the element at index 0. The calls to these methods can be interleaved by concurrent threads executing the removeFirst() method between the time the list size is determined and the first element is removed, unless appropriate steps are taken.

Click here to view code image

// Thread t1                                 // Thread t2
…                                          …
if (name.size() > 0) // true                 …
…                                          if (name.size() > 0) // true

return remove(0);    // Size 0               …
…                                          return remove(0);    // Exception!

The recommended solution is to use a synchronized block on the synchronized list, as shown at (4), to guarantee that the method removeFirst() has exclusive access to the synchronized list, until the individual exclusive operations have completed. Example 23.17 shows output for both scenarios that we have sketched above. Note that the reentrant nature of the intrinsic lock on the synchronized list allows nested locking on the list for the individual exclusive operations.

Example 23.17 Compound Mutually Exclusive Operation

Click here to view code image

package synced;
import java.util.*;
public class DoubleAct {
  // Synchronized list:                                                      (1)
  private List<String> names = Collections.synchronizedList(new ArrayList<>());
  public void add(String name) { names.add(name); }                       // (2)
  public String removeFirst() {                                           // (3)
    synchronized(names) {                                                 // (4)
      if (names.size() > 0) {                                             // (5)
        try { Thread.sleep(1); }                                          // (6)
        catch(InterruptedException e) { e.printStackTrace(); }
        return names.remove(0);                                           // (7)
      } else { return null; }                                             // (8)
    }                                                                     // (9)
  }
}

Click here to view code image

package synced;
import java.util.stream.IntStream;

public class Client {                                                     // (9)
  public static void main(String[] args) {
    DoubleAct da = new DoubleAct();
    da.add(“Laurel”); da.add(“Hardy”);                                    // (10)
    Runnable remover = () -> {                                            // (11)
      String name = da.removeFirst();                                     // (12)
      System.out.println(name);
    };
    IntStream.rangeClosed(1, 3).forEach(i -> new Thread(remover).start());// (13)
  }
}

Probable output from the program:

Laurel
Hardy
null

Probable output from the program when (4) and (9) are commented out (output edited to fit on the page):

Click here to view code image

Laurel
Hardy
Exception in thread “Thread-1” java.lang.IndexOutOfBoundsException:
      Index 0 out of bounds for length 0
      at …
      at synced.DoubleAction.removeFirst(DoubleAction.java:15)
      at …