问题
I have read a lot about the dangers of double checked locking and I would try hard to stay away of it, but with that said I think they make a very interesting read.
I was reading this article of Joe Duffy about implementing singleton with double checked locking: http://www.bluebytesoftware.com/blog/PermaLink,guid,543d89ad-8d57-4a51-b7c9-a821e3992bf6.aspx
And the (variant of) solution he seemed to propose is this:
class Singleton {
private static object slock = new object();
private static Singleton instance;
private static int initialized;
private Singleton() {}
public Instance {
get {
if (Thread.VolatileRead(ref initialized) == 0) {
lock (slock) {
if (initialized == 0) {
instance = new Singleton();
initialized = 1;
}
}
}
return instance;
}
}
}
My question is, doesn't that still have the danger of writes being reordered? Specifically these two lines:
instance = new Singleton();
initialized = 1;
If those writes are inverted, then some other thread can still read null.
回答1:
I think the key is in the linked article (http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S5). Specifically that the MS-implemented .NET 2.0 memory model has the following property:
Writes cannot move past other writes from the same thread.
Duffy mentions that a lot of work was done to support this on the IA-64:
We accomplish this by ensuring writes have 'release' semantics on IA-64, via the st.rel instruction. A single st.rel x guarantees that any other loads and stores leading up to its execution (in the physical instruction stream) must have appeared to have occurred to each logical processor at least by the time x's new value becomes visible to another logical processor. Loads can be given 'acquire' semantics (via the ld.acq instruction), meaning that any other loads and stores that occur after a ld.acq x cannot appear to have occurred prior to the load.
Note that Duffy also mentions that this is an MS-specific guarantee - it's not part of the ECMA spec (at least as of the the article's writing in 2006). So, Mono might not be as nice.
回答2:
Initial Comments
I do not necessarily think the author of that article was actually proposing that this variation of the double-checked locking pattern be used per se. I think he was just pointing that it is one variation that might be considered by an naive developer to tackle the problem in the context of value types.
Value types obviously cannot store null
values so another variable must be used to signal the completion of initialization. The author mentions all of this and then confusingly talks about reading instance
as null
. Presumably, the author was thinking of a really naive developer who used this variation incorrectly on value types at one time and then continued to apply it, also incorrectly, for reference types. In the case of a value type a thread could read and use a struct
with default field initializations when that was not intended. In the case of reference types a thread could read and use null
instance.
The use of Thread.VolatileRead
was the author's proposal to fix this variation. Without the volatile read the read of instance
in the return statement could lifted before the read of initialized
like this.
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;
}
}
}
Hopefully the above reordering of the code clearly demonstrates the problem. And equal obvious it should be that a volatile read of initialized
prevents the read of instance
from being lifted.
And again, I think the author was merely showing one possible way to fix this particular variation and not that the author was advocating this approach in general.
Answering your questions
My question is, doesn't that still have the danger of writes being reordered?
YES (qualified): As your have correctly pointed out the writes to instance
and initialized
could be swapped inside the lock
. Worse still, the writes that may be going on inside Singleton.ctor
could also occur out of order in such a manner that instance
gets assigned before the instance is fully initialized. Another thread could see instance
set, but that instance may be in a partially constructed state.
However, writes in Microsoft's implementation of the CLI have release-fence semantics. Meaning everything I just said does not apply when using the .NET Framework runtime on any hardware platform. But, an obscure environment like Mono running on ARM could exhibit the problematic behavior.
The author's use of Thread.VolatileRead
to "fix" this variation would not work in general because it does nothing to solve the problem of reordered writes. The code is not 100% portable. This is one reason why I doubt the author was proposing this variation.
The canonical variation of using a single instance
variable in conjunction with volatile
is obviously the correct solution. The volatile
keyword has acquire-fence semantics on reads and release-fence semantics on writes so it solves both problems; the one you identified and the one addressed by the article.
回答3:
According to http://msdn.microsoft.com/en-us/library/ee817670.aspx a singleton like
// .NET Singleton
sealed class Singleton
{
private Singleton() {}
public static readonly Singleton Instance = new Singleton();
}
is guaranteed to be thread-safe
The Framework internally guarantees thread safety on static type initialization. [..] In the Framework itself there are several classes that use this type of singleton, although the property name used is called Value instead. The concept is exactly the same.
来源:https://stackoverflow.com/questions/8350713/why-is-this-double-checked-locking-correct-net