Yes, this is correct, and it is present on Wikipedia. (It doesn't matter that the field is volatile, since it is only ever accessed from VarHandle
.)
If the first read sees a stale value, it enters the synchronized block. Since synchronized blocks involve happen-before relationships, the second read will always see the written value. Even on Wikipedia it says sequential consistency is lost, but it refers to the fields; synchronized blocks are sequentially consistent, even though they use release-acquire semantics.
So the second null check will never succeed, and the object is never instantiated twice.
It is guaranteed that the second read will see the written value, because it is executed with the same lock held as when the value was computed and stored in the variable.
On x86 all loads have acquire semantics, so the only overhead would be the null check. Release-acquire allows values to be seen eventually (that's why the relevant method was called lazySet
before Java 9, and its Javadoc used that exact same word). This is prevented in this scenario by the synchronized block.
Instructions may not be reordered out and into synchronized blocks.