My question is an extension to this one: Volatile guarantees and out-of-order execution
To make it more concrete, let's say we have a simple class which can be in two states after it is initialized:
class A { private /*volatile?*/ boolean state; private volatile boolean initialized = false; boolean getState(){ if (!initialized){ throw new IllegalStateException(); } return state; } void setState(boolean newState){ state = newState; initialized = true; } }
The field initialized is declared volatile, so it introduces happen-before 'barrier' which ensures that reordering can't take place. Since the state field is written only before initialized field is written and read only after the initialized field is read, I can remove the volatile keyword from declaration of the state and still never see a stale value. Questions are:
Suppose, instead of the flag, a CountDownLatch was used as an initializer like this:
class A { private /*volatile?*/ boolean state; private final CountDownLatch initialized = new CountDownLatch(1); boolean getState() throws InterruptedException { initialized.await(); return state; } void setState(boolean newState){ state = newState; initialized.countdown(); } }
Would it still be alright?
Your code is (mostly) correct and it is a common idiom.
// reproducing your code class A state=false; //A initialized=false; //B boolean state; volatile boolean initialized = false; //0 void setState(boolean newState) state = newState; //1 initialized = true; //2 boolean getState() if (!initialized) //3 throw ...; return state; //4
Line #A #B are pseudo code for writing default values to variables (aka zeroing the fields). We need to include them in a strict analysis. Note that #B is different from #0; both are executed. Line #B is not considered a volatile write.
All volatile accesses(read/write) on all variables are in a total order. We want to establish that #2 is before #3 in this order, if #4 is reached.
There are 3 writes to initialized
: #B, #0 and #2. Only #2 assigns true. Therefore if #2 is after #3, #3 cannot read true (this is probably due to the no out-of-thin-air guarantee which I don't fully understand), then #4 can't be reached.
Therefore if #4 is reached, #2 must be before #3 (in the total order of volatile accesses).
Therefore #2 happens-before #3 (a volatile write happens-before a subsequent volatile read).
By programming order, #1 happens-before #2, #3 happens-before #4.
By transitivity, therefore #1 happens-before #4.
Line#A, the default write, happens-before everything (except other default writes)
Therefore all accesses to variable state
are in a happens-before chain: #A -> #1 -> #4. There is no data race. The program is correctly synchronized. Read #4 must observe write #1
There is a little problem though. Line #0 is apparently redundant, since #B already assigned false. In practice, a volatile write is not negligible on performance, therefore we should avoid #0.
Even worse, the presence of #0 can cause undesired behavior: #0 can occur after #2! Therefore it may happen that setState()
is called, yet subsequent getState()
keep throwing errors.
This is possible if the object is not safely published. Suppose thread T1 creates the object and publishes it; thread T2 gets the object and calls setState()
on it. If the publication is not safe, T2 can observe the reference to the object, before T1 has finished initializing the object.
You can ignore this problem if you require that all A
objects are safely published. That is a reasonable requirement. It can be implicitly expected.
But if we don't have line #0, this won't be a problem at all. Default write #B must happens-before #2, therefore as long as setState()
is called, all subsequent getState()
will observe initialized==true
.
In the count down latch example, initialized
is final
; that is crucial in guaranteeing safe publication: all threads will observe a properly initialized latch.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With