Reputation: 349
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?
Upvotes: 23
Views: 1141
Reputation: 45433
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.
Upvotes: 8
Reputation: 8295
1. Is this reasoning correct?
No, state will be cached in thread, so you can not get the latest value.
2. Is it guaranteed that the write to initialized field won't be optimized away (since it changes only the first time) and the 'barrier' won't be lost?
Yes
3. Suppose, instead of the flag, a CountDownLatch was used as an initializer like this...
just like @ratchet freak mentioned, CountDownLatch is one time latch, while volatile is kinda of reusable latch, so the answer for your third question should be: If you are going to set the state multiple times you should use volatile.
Upvotes: -1