Initial comments
I donโt necessarily think that the author of this article actually suggested using this variation of the double-checked lock pattern. I think he simply pointed out that this is one option that can be considered by a naive developer to solve the problem in the context of value types.
Value types, obviously, cannot store null values, so you need to use another variable to confirm the completion of initialization. The author mentions all this, and then vaguely talks about reading instance as null . Presumably, the author was thinking of a really naive developer who used this change incorrectly by value type at a time, and then continued to apply it, and also incorrectly, for reference types. In the case of a value type, the stream can read and use struct with initialization of the default field, if it is not intended. In the case of reference types, a stream can read and use an instance of null .
The use of Thread.VolatileRead was proposed by the author to correct this option. Without a volatile read, reading instance in the return statement can be removed before reading initialized as follows.
class Singleton { private static object slock = new object(); private static Singleton instance; private static int initialized; private Singleton() {} public Instance { get { var local = instance; if (initialized == 0) { lock (slock) { if (initialized == 0) { instance = new Singleton(); initialized = 1; } } } return local; } } }
We hope that the aforementioned reordering of the code clearly demonstrates the problem. And it is obvious that an intermittent reading of initialized prevents instance reading.
And again, I think that the author simply showed one of the possible ways to correct this particular option, and not that the author advocated this approach as a whole.
Answering your questions
My question is, is there still a danger that recordings will be ordered?
YES (qualified): since you correctly pointed out that the records in instance and initialized can be replaced inside lock . Worse, entries that can occur inside Singleton.ctor can also fail so that instance assigned before the instance is fully initialized. Another thread might see the instance set, but this instance may be in a partially constructed state.
However, Microsoft entries for CLI implementations have release fence semantics. The meaning of everything I just said does not apply when using the .NET Framework runtime on any hardware platform. But an obscure environment such as Mono running on ARM may exhibit problematic behavior.
An author using Thread.VolatileRead to โfixโ this option will not work at all because he does nothing to solve the reordered write problem. The code is not 100% portable. This is one of the reasons why I doubt that the author suggested this variation.
The canonical variation of using a single instance variable in combination with volatile is obviously the right solution. The volatile keyword has the semantics of capture semantics when reading and the release semantics for writing, so it solves both problems; the one you identified and the one that is listed in the article.