Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java: what is the correct way to guarantee a non-final reference field will never be read as null?

I'm trying to solve a simple problem and am falling into a Java Memory Model rabbit hole.

What is the simplest and/or most efficient (judgement call here), but race-free (precisely defined according to the JMM) way to write a Java class containing a non-final reference field which is initialized to a non-null value in the constructor and subsequently never changed, such that no subsequent access of that field by any other thread can see a non-null value?

Broken starting example:

public class Holder {

  private Object value;

  public Holder(Object value) {
    if (value == null)
        throw NullPointerException();
    this.value = value;
  }

  public Object getValue() {    // this could return null!
    return this.value;
  }
}

And according to this post, marking the field volatile doesn't even work!

public class Holder {

  private volatile Object value;

  public Holder(Object value) {
    if (value == null)
        throw NullPointerException();
    this.value = value;
  }

  public Object getValue() {    // this STILL could return null!!
    return this.value;
  }
}

Is this the best we can do??

public class Holder {

  private Object value;

  public Holder(Object value) {
    if (value == null)
        throw NullPointerException();
    synchronized (this) {
        this.value = value;
    }
  }

  public synchronized Object getValue() {
    return this.value;
  }
}

OK what about this?

public class Holder {

  private Object value;

  public Holder(Object value) {
    if (value == null)
        throw NullPointerException();
    this.value = value;
    synchronized (this) { }
  }

  public synchronized Object getValue() {
    return this.value;
  }
}

Side note: a related question asks how to do this without using any volatile or synchronization, which is of course impossible.

like image 385
Archie Avatar asked Aug 08 '16 22:08

Archie


3 Answers

To safely publish a non-immutable object in Java, You need to synchronise the construction of the object and the write of the shared reference to that object. It's not just the internals of that object that matters in this question.

If you publish an object without proper synchronisation, with reordering, the consumer of a Holder object can still see a partially constructed object if the reference to the object got published before the constructor finishes. For example Double-checked locking without volatile.

There are several ways to publish an object safely:

  • Initialising a reference from a static initializer;
  • Storing a reference to it into a volatile field or AtomicReference
  • Storing a reference to it into a final field of a properly constructed object; Or
  • Storing a reference to it into a field that is properly guarded by a lock.

Note that those bullet points are talking about the reference to the Holder object, not fields of the class.

So the easiest way would be the first option:

public static Holder holder = new Holder("Some value");

Any thread accessing the static field is going to see a properly constructed Holder object.

See Section 3.5.3 "Safe publication idioms" of Java Concurrency in Practice. For more information about unsafe publication, see Section 16.2.1 of Java Concurrency in Practice.

like image 116
xiaofeng.li Avatar answered Oct 19 '22 03:10

xiaofeng.li


The problem you are trying to solve is called safe publication and there exist benchmarks for a best performant solution. Personally, I prefer the holder pattern which also performs the best. Define a Publisher class with a single generic field:

class Publisher<T> {
  private final T value;
  private Publisher(T value) { this.value = value; }
  public static <S> S publish(S value) { return new Publisher<S>(value).value; }
}

You can now create your instance via:

Holder holder = Publisher.publish(new Holder(value));

Since your Holder is dereferenced via a final field, it is guaranteed to be fully initialized by the JMM after reading it from the same final field.

If this is the only usage of your class, then you should of course add a convenience factory to your class and make the constructor itself private to avoid unsafe construction.

Note that this performs very well as modern VMs erase the object allocation after applying escape anaysis. The minimal performance overhead comes from the remaining memory barriers in the generated machine code which are however required to safely publish the instance.

Note: The holder pattern is not to be confused with your example class being called Holder. It is the Publisher that implements the holder pattern in my example.

like image 5
Rafael Winterhalter Avatar answered Oct 19 '22 02:10

Rafael Winterhalter


See section 17.5 of the Java Language Specification.

An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.

In other words, as long as we are careful not to leak this from the constructor of Holder to another thread, we can guarantee that other threads will see the proper (non-null) value of ref without additional synchronization mechanisms.

class Holder {

  private final Object ref;

  Holder(final Object obj) {
    if (obj == null) {
      throw new NullPointerException();
    }
    ref = obj;
  }

  Object get() {
    return ref;
  }
}

If you're seeking a non-final field, recognize we can use synchronized to enforce that get does not return until ref is non-null and also ensure a proper happens-before relationship (see: memory barrier) holds on the wrapped reference:

class Holder {

  private Object ref;

  Holder(final Object obj) {
    if (obj == null) {
      throw new NullPointerException();
    }
    synchronized (this) {
      ref = obj;
      notifyAll();
    }
  }

  synchronized Object get() {
    while (ref == null) {
      try {
        wait();
      } catch (final InterruptedException ex) { }
    }
    return ref;
  }
}
like image 2
obataku Avatar answered Oct 19 '22 01:10

obataku