Неправильная публикация ссылки на объект Java - программирование
Подтвердить что ты не робот

Неправильная публикация ссылки на объект Java

Ниже приведен пример книги "Java Concurrency на практике" Брайана Гетца, глава 3, раздел 3.5.1. Это пример ненадлежащей публикации объектов

class someClass {
    public Holder holder;

    public void initialize() {
        holder = new Holder(42);
    }
}

public class Holder {
  private int n;
  public Holder(int n) { this.n = n; }

  public void assertSanity() {
    if (n!=n)
      throw new AssertionError("This statement is false");
  }
}

В нем говорится, что держатель мог появиться в другом потоке в несогласованном состоянии, а другой поток мог бы наблюдать частично построенный объект. Как это может произойти? Не могли бы вы дать сценарий, используя приведенный выше пример?

Далее следует, что бывают случаи, когда поток может видеть устаревшее значение при первом чтении поля, а затем в более позднем значении в следующий раз, поэтому assertSanity может вызывать Assertion Error. Как может быть вызвано assertionError?

Из дальнейшего чтения один из способов устранить эту проблему состоит в том, чтобы сделать Холдер неизменным, сделав переменную 'n' final. Предположим теперь, что Холдер не является непоколебимым, но эффективно неизменным. Чтобы безопасно публиковать этот объект, нам нужно сделать статическую инициализацию владельца и объявить ее изменчивой (как статической, так и изменчивой или просто изменчивой)? Что-то вроде

public class someClass {
    public static volatile Holder holder = new Holder(42);

}

Спасибо за вашу помощь заранее.

4b9b3361

Ответ 1

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

Хорошо, что JMM не имеет правил для записи и публикации полей элемента holder - до записи поля holder, как это происходит в initialie(). Это означает, что даже если holder не является нулевым, законным для полей-членов еще не быть видимым для других потоков.

Вы можете увидеть что-то вроде

public class Holder{
    String someString = "foo";
    int someInt = 10;
} 

holder может быть не нулевым, но someString может быть нулевым, а someInt может быть 0.

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

Итак, следующий вопрос может быть Why does volatile fix this? JMM говорит, что все записи, которые происходят до энергозависимого хранилища, видны для всех последующих потоков волатильного поля.

Итак, если holder является изменчивым, и вы видите, что holder не является нулевым, на основе правил volatile, все поля будут инициализированы.

Чтобы безопасно публиковать этот объект, мы должны сделать держателя инициализация статическая и объявить ее изменчивой

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

Как может быть вызвано assertionError?

Если поток замечает holder не равным нулю и вызывает assertionError при вводе метода и чтении n, первый раз может быть 0 (значение по умолчанию), второе чтение n теперь может видеть запись из первого потока.

Ответ 2

public class Holder {
  private int n;
  public Holder(int n) { this.n = n; }

  public void assertSanity() {
    if (n!=n)
      throw new AssertionError("This statement is false");
  }
}

Скажем, что один поток создает экземпляр Holder и передает ссылку на другой поток, который вызывает assertSanity.

Назначение this.n в конструкторе происходит в одном потоке. И два чтения n встречаются в другом потоке. Единственное, что происходит - до отношения между двумя чтениями. Не происходит - до отношения, связанного с назначением, и любого из чтений.

Без каких-либо действий - до отношений операторы могут быть переупорядочены различными способами, поэтому с точки зрения одного потока this.n = n может возникнуть после возврата конструктора.

Это означает, что назначение может появиться во втором потоке после первого чтения и перед вторым, что приводит к несогласованным значениям. Это можно предотвратить, сделав n final, который гарантирует, что значение назначено до завершения конструктора.

Ответ 3

Проблема, о которой вы спрашиваете, вызвана оптимизацией JVM и тем, что простое создание объекта:

MyClass obj = new MyClass()

не всегда выполняется с помощью шагов:

  • Резервная память для нового экземпляра MyClass в куче
  • Выполнить конструктор для задания внутренних значений свойств
  • Установите ссылку 'obj' на адрес в куче

Для некоторых целей оптимизации JVM может сделать это с помощью шагов:

  • Резервная память для нового экземпляра MyClass в куче
  • Установите ссылку 'obj' на адрес в куче
  • Выполнить конструктор для задания внутренних значений свойств

Итак, представьте, хотите ли два потока получить доступ к объекту MyClass. Сначала создается, но из-за JVM он выполняет "оптимизированный" набор шагов. Если он выполнит только шаги 1 и 2 (но не сделает 3), то у нас может возникнуть серьезная проблема. Если второй поток использует этот объект (он не будет равен нулю, поскольку он уже указывает на зарезервированную часть памяти в куче), чем свойства будут неправильными, что может привести к неприятным вещам.

Эта оптимизация не произойдет, если ссылка будет неустойчивой.

Ответ 4

Класс Holder в порядке, но класс someClass может отображаться в несогласованном состоянии - между созданием и вызовом initialize() переменной экземпляра Holder является null.