The fallowing clause is taken from jetbrains.net After reading this and some other articles on the web, I still don't understand how is it possible to return null, after the first thread go in to the lock. Some one that does understand it can please help me and explain it in more humanized way?
"Consider the following piece of code:
public class Foo
{
private static Foo instance;
private static readonly object padlock = new object();
public static Foo Get()
{
if (instance == null)
{
lock (padlock)
{
if (instance == null)
{
instance = new Foo();
}
}
}
return instance;
}
};
Given the above code, writes that initialize the Foo instance could be delayed until the write of the instance value, thus creating the possibility that the instance returns an object in an unitialized state.
In order to avoid this, the instance value must be made volatile. "
Bill Pugh has written several articles on the subject, and is a reference on the topic.
A notable reference is, The "Double-Checked Locking is Broken" Declaration.
Roughly speaking, here is the problem:
In mutlicore VM, writes by a thread might not be visible to other thread until a synchronization barrier (or memory fences) is reached. You can read "Memory Barriers: a Hardware View for Software Hackers" it's a really good article on the matter.
So, if a thread initializes an object
A
with one fielda
, and stores the reference of the object in the fieldref
of another objectB
, we have two "cells" in memory:a
, andref
. Changes to both memory locations might not become visible to other threads at the same time unless the threads forces the visiblity of the changes with a memory fence.In java, synchronization can be forced with
synchronized
. This is expensive, and an alternative it to declare a field asvolatile
in which case the change to this cell is always visible to all threads.BUT, the semantics of volatile change between Java 4 and 5. In Java 4, you need to define both
a
, andref
as volatile, for the doulbe check to work in the example I described.It was not intuitive, and most people would only set
ref
as volatile. So they change this and in Java 5+, if a volatile field is modified (ref
) it triggers the synchronization of other fields modified (a
).EDIT: I only see now that you ask for C#, not Java... I leave my answer because maybe it's useful nevertheless.
Returning
null
is not the issue. The issue is that the new instance may be in a partially constructed state as perceived by another thread. Consider this declaration ofFoo
.Here is how the code could get optimized by the C# compiler, JIT compiler, or hardware.1
First, notice that the constructor is inlined (because it was simple). Now, hopefully it is easy to see that
instance
gets assigned the reference before its constituent fields get initialized inside the constructor. This is a valid strategy because reads and writes are free to float up and down as long as they do not pass the boundaries of thelock
or alter the logical flow; which they do not. So another thread could seeinstance != null
and attempt to use it before it is fully initialized.volatile
fixes this issue because it treats reads as an acquire fence and writes as a release fence.So if we mark
instance
asvolatile
then the release-fence will prevent the above optimization. Here is how the code would look with the barrier annotations. I used an ↑ arrow to indicate a release-fence and a ↓ arrow to indicate an acquire-fence. Notice that nothing is allowed to float down past an ↑ arrow or up past an ↓ arrow. Think of the arrow head as pushing everything away.The writes to the constituent variables of
Foo
could still be reordered, but notice that the memory barrier now prevents them from occurring after the assignment toinstance
. Using the arrows as a guide imagine various different optimization strategies that are allowed and disallowed. Remember that no reads or writes are allowed to float down past an ↑ arrow or up past an ↓ arrow.Thread.VolatileWrite
would have solved this problem as well and could be used in languages without avolatile
keyword like VB.NET. If you take a look at howVolatileWrite
is implemented you would see this.Now this may seem counter intuitive at first. Afterall, the memory barrier is placed before the assignment. What about getting the assignment committed to main memory you ask? Would it not be more correct to place the barrier after the assignment? If that is what your intuition is telling you then it is wrong. You see memory barriers are not strictly about getting a "fresh read" or a "committed write". It is all about instruction ordering. This is by far the biggest source of confusion I see.
It might also be important to mention that
Thread.MemoryBarrier
actually generates a full-fence barrier. So if I were to use my notation above with the arrows then it would look like this.So technically calling
VolatileWrite
does more than what a write to avolatile
field would do. Remember thatvolatile
is not allowed in VB.NET for example, butVolatileWrite
is apart of the BCL so it can be used in other languages.1This optimization is mostly theoretical. The ECMA specification does technically allow for it, but the Microsoft CLI implementation of the ECMA specification treats all writes as if they had release fence semantics already. It is possible that another implementation of the CLI could still perform this optimization though.