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):
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.