Recently I have read a great tutorial of Java Memory Model. It says JVM only guarantees the visibility of final field if no synchronization used. Then it occurs to me that when we use some IoC frameworks, we usually use setter injection/field injection which are not protected by final semantics. For example,
class SomeController { private SomeService service; @Inject public void setService(SomeService s){ this.service = s; } }
If it is possible for some thread to read a stale value of service
after the injection? Or should we mark service
as a volatile field?
Advertisement
Answer
First of all you are reading a “tutorial” (that is a rather weird name for such a complicated topic) that is really old. Also, that document is targeted towards people that (usually) write compilers or work around the JVM itself; I still find it an excellent write-up.
You are correct that visibility is guaranteed under special conditions; but final
is only one of them. There are at least 3 (and not limited to):
Use a proper locked field
Use static initializers
Use a
volatile
field.
In the end, this is called “safe publishing” and it is all about how callers, given a reference to an instance of SomeController
, will perceive its fields (service
). Are they guaranteed to see a non-null service
?
Spring guarantees that it will be a fully initialized instance, but not in the sense that you might think. There is a principle in the JLS
called “happens-before”. It is also called a happens-before “relationship”, since it involves two parties. For example one that does a write (calls that setService
) and one that does a read (uses that service
). It is said that the relationship is guaranteed and fulfilled (reading part sees a non-null service
) when both parties follow some rules. Those rules are very strictly written in the JLS. In simpler words: you are guaranteed to see a non-null service
only when one of those rules are followed. One of them is mentioned by you:
A write to a volatile field happens-before every subsequent read of that field.
But notice that it is not the only one there.
So, if Spring, for example, does all the injections in a Thread, and only after that calls Thread::start
on it’s context, then there is a rule in the JLS here
A call to start() on a thread happens-before any actions in the started thread.
that will guarantee that service
is injected and correctly seen as non-null.
This probably needs a bit more explanation here, so here is an example:
// (1) init Spring context and do the needed injections // (2) call Thread::start with this context // (3) use context in a different thread now
There are three rules that we need to follow here from that JLS document:
If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).
This means that (1) happens-before (2)
A call to start() on a thread happens-before any actions in the started thread.
This means (2) happens-before (3).
If hb(x, y) and hb(y, z), then hb(x, z).
This means (1) happens-before (3). And this is the one we care about and it’s just one way Spring can achieve proper visibility.