Java: какой правильный способ гарантировать не конечное поле ссылки никогда не будет считаться нулевым?

Я пытаюсь решить простую проблему и попадаю в отверстие для кроликов Java-памяти.

Что является самым простым и/или наиболее эффективным (звонок суждения здесь), но гонка бесплатно (точно определены в соответствии с ЯВМ) способом написания класса Java, содержащий <сильного > неконечного ссылки на поле который инициализируется ненулевым значением в конструкторе и впоследствии никогда не изменяется, так что никакой последующий доступ этого поля ни к какому другому потоку не может видеть ненулевое значение?

Пример неудачного запуска:

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;
  }
}

И в соответствии с этот пост, обозначение поля volatile даже не работает!

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;
  }
}

Это лучшее, что мы можем сделать?

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;
  }
}

Как насчет этого?

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;
  }
}

Боковое примечание: связанный вопрос спрашивает, как это сделать без использования volatile или синхронизации, что, конечно, невозможно.

Ответ 1

Проблема, которую вы пытаетесь решить, называется безопасной публикацией, и существуют теги для лучшего решения для исполнителей. Лично я предпочитаю шаблон держателя, который также работает лучше всего. Определите класс Publisher с одним общим полем:

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; }
}

Теперь вы можете создать свой экземпляр с помощью:

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

Так как ваш Holder разыменовывается с помощью поля final, он может быть полностью инициализирован JMM после прочтения его из того же конечного поля.

Если это единственное использование вашего класса, вы должны, конечно, добавить удобство factory в свой класс и сделать сам конструктор private, чтобы избежать небезопасной конструкции.

Обратите внимание, что это очень хорошо работает, поскольку современные виртуальные машины стирают выделение объектов после применения escape-анейса. Минимальные эксплуатационные издержки на производительность связаны с остальными барьерами памяти в сгенерированном машинный код, которые, тем не менее, необходимы для безопасного опубликования экземпляра.

Примечание: шаблон держателя не следует путать с тем, что ваш класс класса называется Holder. Это Publisher реализует шаблон держателя в моем примере.

Ответ 2

Чтобы безопасно публиковать непеременный объект в Java, вам необходимо синхронизировать конструкцию объекта и запись общей ссылки на этот объект. Это не только внутренности этого объекта, в этом вопросе.

Если вы публикуете объект без надлежащей синхронизации, с переупорядочением, потребитель объекта Holder все еще может видеть частично сконструированный объект, если ссылка на объект была опубликована до завершения конструктора. Например Двойная проверка блокировки без volatile.

Существует несколько способов безопасного размещения объекта:

  • Инициализация ссылки из статического инициализатора;
  • Сохранение ссылки на него в поле volatile или AtomicReference
  • Сохранение ссылки на него в конечное поле правильно построенного объекта; Или
  • Сохранение ссылки на него в поле, которое должным образом защищено блокировкой.

Обратите внимание, что эти точки маркера говорят о ссылке на объект Holder, а не на поля этого класса.

Таким образом, самый простой способ - это первый вариант:

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

Любой поток, обращающийся к статическому полю, увидит правильно построенный объект Holder.

См. раздел 3.5.3 "Идиомы безопасной публикации" Java Concurrency на практике. Дополнительную информацию о небезопасной публикации см. В разделе 16.2.1 Java Concurrency на практике.

Ответ 3

См. раздел 17.5 Спецификации языка Java.

Объект считается полностью инициализированным, когда его конструктор заканчивается. Поток, который может видеть только ссылку на объект после того, как этот объект был полностью инициализирован, гарантированно увидит правильно инициализированные значения для окончательных полей этого объекта.

Другими словами, если мы стараемся не просачивать this из конструктора Holder в другой поток, мы можем гарантировать, что другие потоки будут видеть правильное (не null) значение ref без дополнительных механизмов синхронизации.

class Holder {

  private final Object ref;

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

  Object get() {
    return ref;
  }
}

Если вы ищете не конечное поле, узнайте, что мы можем использовать synchronized для обеспечения того, чтобы get не возвращался до тех пор, пока ref не был бы нулевым, а также обеспечил бы правильное происхождение-до отношения (см. барьер памяти) выполняется на завернутой ссылке:

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;
  }
}

Ответ 4

Невозможно гарантировать, что не конечная ссылка никогда не будет равна нулю.

Даже если вы правильно его инициализируете и не гарантируете в setter, по-прежнему можно установить ссылку на нуль через отражение.

Вы можете ограничить шансы вернуть нулевую ссылку, объявив получателя окончательным и никогда не возвращая null из получателя.

Это; однако все же можно переопределить конечный геттер и заставить его вернуть нуль. Вот ссылка, которая описывает, как высмеивать окончательный метод: Окончательный метод издевательства

Если они могут высмеять окончательный метод, любой может, используя тот же метод, переопределить конечный метод и сделать его плохо функционирующим.