The code below (Java Concurrency in Practice listing 16.3) is not thread safe for obvious reasons:
public class UnsafeLazyInitialization {
private static
After applying the JLS rules to this example, I have come to the conclusion that getInstance can definitely return null. In particular, JLS 17.4:
The memory model determines what values can be read at every point in the program. The actions of each thread in isolation must behave as governed by the semantics of that thread, with the exception that the values seen by each read are determined by the memory model.
It is then clear that in the absence of synchronization, null is a legal outcome of the method since each of the two reads can observe anything.
Decomposition of reads and writes
The program can be decomposed as follows (to clearly see the reads and writes):
Some Thread
---------------------------------------------------------------------
10: resource = null; //default value //write
=====================================================================
Thread 1 | Thread 2
----------------------------------+----------------------------------
11: a = resource; | 21: x = resource; //read
12: if (a == null) | 22: if (x == null)
13: resource = new Resource(); | 23: resource = new Resource(); //write
14: b = resource; | 24: y = resource; //read
15: return b; | 25: return y;
What the JLS says
JLS 17.4.5 gives the rules for a read to be allowed to observe a write:
We say that a read r of a variable v is allowed to observe a write w to v if, in the happens-before partial order of the execution trace:
- r is not ordered before w (i.e., it is not the case that hb(r, w)), and
- there is no intervening write w' to v (i.e. no write w' to v such that hb(w, w') and hb(w', r)).
Application of the rule
In our example, let's assume that thread 1 sees null and properly initialises resource. In thread 2, an invalid execution would be for 21 to observe 23 (due to program order) - but any of the other writes (10 and 13) can be observed by either read:
So both 21 and 24 (our 2 reads) are allowed to observe either 10 (null) or 13 (not null).
Execution path that returns null
In particular, assuming that Thread 1 sees a null on line 11 and initialises resource on line 13, Thread 2 could legally execute as follows:
24: y = null (reads write 10)21: x = non null (reads write 13)22: false25: return yNote: to clarify, this does not mean that T2 sees non null and subsequently sees null (which would breach the causality requirements) - it means that from an execution perspective, the two reads have been reordered and the second one was committed before the first one - however it does look as if the later write had been seen before the earlier one based on the initial program order.
UPDATE 10 Feb
Back to the code, a valid reordering would be:
Resource tmp = resource; // null here
if (resource != null) { // resource not null here
resource = tmp = new Resource();
}
return tmp; // returns null
And because that code is sequentially consistent (if executed by a single thread, it will always have the same behaviour as the original code) it shows that the causality requirements are satisfied (there is a valid execution that produces the outcome).
After posting on the concurrency interest list, I got a few messages regarding the legality of that reordering, which confirm that null is a legal outcome:
- The transformation is definitely legal since a single-threaded execution won't tell the difference. [Note that] the transformation doesn't seem sensible - there's no good reason a compiler would do it. However, given a larger amount of surrounding code or perhaps a compiler optimization "bug", it could happen.
- The statement about intra-thread ordering and program order is what made me question the validity of things, but ultimately the JMM relates to the bytecode that gets executed. The transformation could be done by the javac compiler in which case null will be perfectly valid. And there are no rules for how javac has to convert from Java source to Java bytecode so...