Serial Access

In order to guarantee thread-safety, all access to elements of the underlying collection should be through the synchronized collection. Although getter and setter methods are synchronized in a synchronized collection, this is not the case for methods that implement serial access (e.g., iteration using an iterator or a stream).

As multiple methods can be called for an operation on a synchronized collection when using the iterator, we need to ensure that these are executed as a single mutually exclusive operation. Iteration thus requires a coarse-grained manual synchronization on the synchronized collection for deterministic results. A more general case of coarse-grained synchronization is discussed in the next subsection (p. 1480).

Example 23.16 illustrates the idiom used for serial access over a synchronized collection. A synchronized list is created at (1) and populated. A Runnable is implemented at (2) to remove a certain year from the synchronized list.

Serial access over the synchronized list is attempted using an explicit iterator. However, serial access operations are all done in a synchronized block requiring the intrinsic lock on the synchronized list, as at (3). Obtaining the iterator is also done in the synchronized block. All modifications must be made through the iterator. Iterator methods are used to process the synchronized list, including removing elements. Keep in mind that next() and remove() methods of the iterator work in lockstep. The current element from each iteration can be processed safely. This idiom also applies when using a for(:) loop for serial access, as this loop internally is translated to an iterator.

The program in Example 23.16 is run with three threads that execute the Runnable eliminator. With manual synchronization on the synchronized list at (3), the threads execute as expected. One of the threads removed the year 2021 from the list and thus modified the list. As there was only one occurrence of the year 2021, the other two threads did not modify the list. With no manual synchronization on the synchronized list, the results are unpredictable. Most likely one or more exceptions will be thrown.

Regardless of manually synchronizing on the synchronized collection, as in Example 23.16, any modification made directly on the underlying collection during serial access will result in the runtime java.util.ConcurrentModificationException.

Example 23.16 Serial Access in Synchronized Views of Collections

Click here to view code image

package synced;
import java.util.*;
import java.util.stream.IntStream;
public class SerialAccessThreads  {
  public static void main(String[] args) throws InterruptedException {
    List<Integer> years = Collections.synchronizedList(new ArrayList<>()); // (1)
    years.add(2024); years.add(2023); years.add(2021); years.add(2022);
    Runnable eliminator = () -> {                                          // (2)
      boolean found = false;
      synchronized(years) {                                                // (3)
        Iterator<Integer> iteratorA = years.iterator();                    // (4)
        while (iteratorA.hasNext()) {
          if (iteratorA.next().equals(2021)) {                             // (5)
            iteratorA.remove();                                            // (6)
            found = true;
          }
        }
      }                                                                    // (7)
      System.out.println(“List modified: ” + found);
    };
    IntStream.rangeClosed(1, 3).forEach(i -> new Thread(eliminator).start());
  }
}

Probable output from the program with manual synchronization for serial access:

List modified: false
List modified: true
List modified: false

Probable output from the program without manual synchronization for serial access (comment out lines at (2) and (7)) (output edited to fit on the page):

Click here to view code image

Exception in thread “Thread-1”
Exception in thread “Thread-2”
Exception in thread “Thread-0” java.lang.NullPointerException:
    Cannot invoke “java.lang.Integer.equals(Object)” because the return value of
    “java.util.Iterator.next()” is null
    …