Search code examples
javamultithreadingconcurrencysynchronizationmemory-model

Double-Check Idiom using booleans


Take the following java code:

public class SomeClass {
  private boolean initialized = false;
  private final List<String> someList; 

  public SomeClass() {
    someList = new ConcurrentLinkedQueue<String>();
  }

  public void doSomeProcessing() {
    // do some stuff...
    // check if the list has been initialized
    if (!initialized) {
      synchronized(this) {
        if (!initialized) {
          // invoke a webservice that takes a lot of time
          final List<String> wsResult = invokeWebService();
          someList.addAll(wsResult);
          initialized = true;
        }
      } 
    }
    // list is initialized        
    for (final String s : someList) {
      // do more stuff...
    }
  }
}

The trick is that doSomeProcessing gets invoked only under certain conditions. Initializing the list is a very expensive procedure and it might not be needed at all.

I have read articles on why the double-check idiom is broken and I was a bit skeptic when I saw this code. However, the control variable in this example is a boolean, so a simple write instruction is needed, as far as I know.

Also, please notice that someList has been declared as final and keeps a reference to a concurrent list, whose writes happen-before reads; if instead of a ConcurrentLinkedQueue the list were a simple ArrayList or LinkedList, even though it has been declared as final, the writes don't require to happen-before the reads.

So, is the code given above free of data races?


Solution

  • Ok, let's get the Java Language Specification. Section 17.4.5 defines happens-before as follows:

    Two actions can be ordered by a happens-before relationship. If one action happens-before another, then the first is visible to and ordered before the second. If we have two actions x and y, we write hb(x, y) to indicate that x happens-before y.

    • If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).
    • There is a happens-before edge from the end of a constructor of an object to the start of a finalizer (§12.6) for that object.
    • If an action x synchronizes-with a following action y, then we also have hb(x, y).
    • If hb(x, y) and hb(y, z), then hb(x, z).

    It should be noted that the presence of a happens-before relationship between two actions does not necessarily imply that they have to take place in that order in an implementation. If the reordering produces results consistent with a legal execution, it is not illegal.

    It then goes on two discuss:

    More specifically, if two actions share a happens-before relationship, they do not necessarily have to appear to have happened in that order to any code with which they do not share a happens-before relationship. Writes in one thread that are in a data race with reads in another thread may, for example, appear to occur out of order to those reads.

    In your instance, the thread checking

    if (!initialized)
    

    may see the new value for initialized before it sees all writes that added to someList and hence work with a partially filled list.

    Note that your argument

    Also, please notice that someList has been declared as final and keeps a reference to a concurrent list, whose writes happen-before reads

    is irrelavant. Yes, if the thread read a value from the list, we could conclude that he also sees anything that happens-before that the write of that value. But what if it doesn't read a value? What if the list appears empty? And even if it read a value, it doesn't mean that subsequent writes have been performed, and hence the list may appear incomplete.