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

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

Каков самый простой и/или наиболее эффективный (здесь вызов решения), но свободный от гонки (точно определенный в соответствии с JMM) способ написать класс 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 или синхронизации, что конечно же невозможно.


person Archie    schedule 08.08.2016    source источник
comment
Как насчет AtomicReference?   -  person Andy Turner    schedule 09.08.2016
comment
Можно ли использовать @NonNull private Object value; из Java 8?   -  person 4castle    schedule 09.08.2016
comment
В каком случае метод getValue() в первом примере может вернуть null? кроме того, если вы планируете присвоить значение только один раз в конструкторе, а затем просто прочитать его - похоже, вы ищете неизменяемый объект, в таком случае - почему бы не объявить его как final ? (Я вижу, что вы написали нефинал только потому, что не предоставили хорошую мотивацию...)   -  person Nir Alfasi    schedule 09.08.2016
comment
getValue() не может возвращать значение null, если в конструкторе нет выхода this, которого нет. В противном случае любой вызов getValue() должен следовать за всем конструктором либо в том же потоке, либо в потоке, запущенном тем же потоком после возврата из конструктора, и JLS #17.4;5 гарантирует hb(x,y), если < i>x и y — это последовательные действия в одном потоке.   -  person user207421    schedule 09.08.2016
comment
Если он инициализируется в конструкторе и никогда не изменяется, почему бы не сделать его окончательным?   -  person Bohemian♦    schedule 09.08.2016
comment
@Bohemian Бывают ситуации, когда вы не можете создать поле final, например, когда вы реализуете Cloneable и вам нужно глубоко клонировать некоторые поля.   -  person Archie    schedule 09.08.2016
comment
@EJP, почему или в потоке, запущенном тем же потоком после возврата конструкции? Поток мог уже начаться, и ссылка this была передана ему потоком, вызвавшим конструктор.   -  person Archie    schedule 09.08.2016
comment
@Арчи Совершенно верно. Но ссылка this существует только после возврата конструктора, и действие по передаче ее другому потоку будет составлять y в терминах цитаты JLS.   -  person user207421    schedule 09.08.2016
comment
@EJP Какой у тебя источник для этого?   -  person shmosel    schedule 09.08.2016
comment
Ссылка this может escape конструктора передаваться методу, вызываемому внутри конструктора, а это означает, что присвоение полю value также может быть невидимым для другого потока. Но в исходном вопросе такой утечки нет.   -  person PNS    schedule 09.08.2016
comment
так что ни один последующий доступ к этому полю любым другим потоком не может увидеть ненулевое значение. Вы имели в виду нулевое значение?   -  person Hulk    schedule 09.08.2016
comment
Идея состоит в том, что какой-то поток T1 вызывает конструктор Holder, конструктор возвращается, а затем T1 делает новый объект Holder видимым для другого потока T2, который затем вызывает get(); в этом случае get() может вернуть null (возможно только потому, что поле не final).   -  person Archie    schedule 09.08.2016
comment
Если это так, то явное утверждение спецификации JVM о том, что объект считается полностью инициализированным, когда его конструктор завершает работу, неверно и применимо только к конечным полям, как следует из следующей фразы в том же документе.   -  person PNS    schedule 09.08.2016
comment
Правильно - это утверждение относится только к конечным полям. В этом и загвоздка.   -  person Archie    schedule 10.08.2016


Ответы (4)


Проблема, которую вы пытаетесь решить, называется безопасной публикацией, и существуют эталоны для наилучшего эффективное решение. Лично я предпочитаю шаблон держателя, который также работает лучше всего. Определите класс 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 после считывания его из того же конечного поля.

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

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

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

person Rafael Winterhalter    schedule 09.08.2016
comment
Почти уверен, что это не сработает по той же причине, что и volatile Holder v; Holder holder = v = new Holder(value);. Бессмысленно создавать отношение происходит до между действиями в одном потоке, потому что оно уже существует неявно. Это работает только в том случае, если другой поток действительно читает переменную final или volatile, как в примере, на который вы ссылаетесь. - person shmosel; 10.04.2018

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

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

Есть несколько способов безопасно опубликовать объект:

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

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

Так что проще всего будет первый вариант:

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

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

См. раздел 3.5.3 «Идиомы безопасной публикации» книги Параллелизм Java на практике. Дополнительные сведения о небезопасной публикации см. в разделе 16.2.1 документа Java Concurrency на практике.

person xiaofeng.li    schedule 09.08.2016
comment
Хороший звонок. Безопасная публикация — это способ гарантировать, что поле установлено. Без безопасной публикации Holder даже synchronized не обеспечит видимость своего поля; если volatile недостаточно, то и synchronized. С другой стороны, если Holder безопасно опубликовано и фактически неизменяемо, поле не нуждается в какой-либо специальной обработке. - person erickson; 09.08.2016

См. раздел 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;
  }
}
person obataku    schedule 08.08.2016
comment
В вопросе явно указывается класс Java, содержащий поле ссылки non final, поэтому утверждение, что он работает для конечных полей, не является ответом на вопрос. Даже в заголовке написано нефинальное справочное поле. Для меня это означает, что OP знает, что он работает для конечных полей, но спрашивает, как заставить его работать для неконечных полей. Таким образом, этот ответ бесполезен. - person Andreas; 09.08.2016
comment
Это должно работать. Но я сомневаюсь, что кто-то на самом деле пишет классы таким образом в реальной жизни. - person xiaofeng.li; 09.08.2016
comment
@LukeLee хочешь уточнить? - person obataku; 09.08.2016
comment
ИМХО, это слишком дорого стоит. Но для чего? Чтобы пользователь этого класса мог не следовать безопасной практике публикации? - person xiaofeng.li; 09.08.2016

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

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

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

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

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

person DwB    schedule 08.08.2016
comment
Можно также изменить значение конечного поля экземпляра с помощью отражения, но в исходном вопросе такого варианта использования нет. - person PNS; 09.08.2016
comment
Этот вопрос не про взлом объекта, а про многопоточный доступ к объекту. - person Andreas; 09.08.2016
comment
Вот что я тоже сказал: в исходном вопросе такого варианта использования нет. Я просто добавлял немного больше информации об отражении по сравнению с окончательным модификатором, учитывая приведенный выше ответ. - person PNS; 09.08.2016