Now, before some zealot quotes Knuth forgetting what he spent his life on - the question has a mostly educational purpose, as I struggle to understand memory barriers. Lets assume:
public class Var<T> {
private T finalized = null;
private boolean isFinal = false;
private T init;
public Var(T init) { this.init = init; }
public T getFinal() { return finalized; }
public T get() { return init; }
public void set(T val) {
if (isSet)
throw new IllegalStateException();
if (val == null)
throw new NullPointerException(); //null means finalized is uninitialized.
init = val;
}
public void freeze() {
isSet = true;
if (!field.compareAndSet(this, null, init))
throw new IllegalStateException();
}
private static void field =
MethodHandles.lookup().findVarHandle(Var.class, "finalized", Object.class);
}
The idea is that a single thread accesses the object in its initial, mutable state, calling get
and set
. Afterwards it calls freeze
and makes the object available
to other threads, which use only getFinal
. I want to guarantee those threads see
the frozen value. I don't care what happens if in the mutable state multiple threads access it - it is considered not thread safe at that stage.
The questions are:
- do I need an additional memory barrier in
getFinal
? - do I need it if
T
is immutable (contains only final fields)? - if I share a reference to a
Var
before callingfreeze
, would agetVolatile
ingetFinal
change things? Lets assume againT
is immutable, as otherwise, as I understand, reader threads could see it in an unitialized state.
CodePudding user response:
Memory barriers are not a sane mental model for Java. For Java, you need to understand the Java Memory Model.
What you need to ensure is that you should not have a data race; so there should be some happens-before edge between a write and a read (or another write).
In your case, you need to make sure there is a happens-before edge between the construction and configuration of the MyObject-instance, and reading the MyObject-instance. The simplest approach would be:
class MyObject{int a,int b;}
class MyObjectContainer{
volatile MyObject value;
void set(MyObject value){this.value = value;}
MyObject get(){return value;}
}
MyObject object = new MyObject();
object.setA(10)
object.setB(20)
myObjectContainer.set(object);
And a different thread can then call myObjectContainer.get() safely.
The 'value' needs to be volatile. Otherwise, there would be a data race. This is because volatile generates the happens-before edge (volatile variable rule) required. In combination with the program order rule and the transitivity rule, we get a guaranteed happens-before edge. The above approach is also known under the name 'safe publication'.
The above will only work if the MyObject instance is not modified after it has been written to the myObjectContaier. So it is 'effectively' immutable.
On the X86 a volatile-read is extremely cheap because every load already has acquire-semantics. A volatile-read only restricts compiler optimizations (compiler fence) and is not a CPU memory fence. The burden on the X86 is on the volatile store.
In exceptional cases, the volatile-store can be a performance bottleneck because it effectively stalls the loads till the store buffer has been drained. In those cases, you can play with relaxed memory ordering (so officially they are classified as data races) using either Unsafe or the VarHandle. In your case, an acquire-load and release-store would be the first steps down the rabbit hole. Depending on the situation, even a [StoreStore] fence before an opaque-store and an [LoadLoad] after an opaque-load would be as low as you could possibly get.
CodePudding user response:
Do I need getVolatile memory access semantics if the value is set with setVolatile?
Anytime an object is shared between threads you need to worry about memory synchronization.
if (!field.compareAndSet(this, null, init))
I think maybe you are confusing field.compareAndSet(...)
with AtomicReference.compareAndSet(...)
. Just because they have the same name doesn't mean that there is added memory protection added just because you are accessing the method through reflection.
do I need an additional memory barrier in getFinal?
You need memory barrier protection, yes. I'm not sure of your use of "additional".
If an instance of Var
is shared between threads such that one thread calls set()
and another calls get()
then init
needs to be volatile
. If one thread calls freeze
and another calls getFinal()
then finalized
needs to be volatile
as well.
do I need it if T is immutable (contains only final fields)?
The T
object will not be shared in an inconsistent state if all fields are final
but that does not protect the Var
object. One thread could set the immutable object T
and then another thread could call get
and still see the init
field as `null.
if I share a reference to a Var before calling freeze, would a getVolatile in getFinal change things?
That doesn't change anything.