Подтвердить что ты не робот

Что именно означает окончательное ключевое слово в отношении concurrency?

Я думаю, что я прочитал, что последнее ключевое слово в поле гарантирует, что если поток 1 создает экземпляр объекта, содержащего это поле, тогда поток 2 всегда будет видеть инициализированное значение этого поля, если поток 2 имеет ссылку на объект ( при условии, что она была правильно построена). В JLS также говорится, что

[Thread 2] также будет видеть версии любого объекта или массива, на которые ссылаются те конечные поля, которые, по крайней мере, являются последними как конечные поля находятся. (раздел 17.5 JLS)

Это означает, что если у меня есть класс A

class A {
  private final B b = new B();
  private int aNotFinal = 2;
  ...

и класс B

class B {
  private final int bFinal = 1;
  private int bNotFinal = 2;
  ...

то aNotFinal не может быть инициализирован потоком времени 2, который получает ссылку на класс A, но поле bNotFinal является, поскольку B является объектом, на который ссылается конечное поле, как указано в JLS.

Есть ли у меня это право?

Edit:

Сценарий, в котором это может произойти, будет состоять в том, что если у нас есть два потока, одновременно выполняющих getA() в одном экземпляре класса C

class C {
  private A a;

  public A getA(){
    if (a == null){
      // Thread 1 comes in here because a is null. Thread B doesn't come in 
      // here because by the time it gets here, object c 
      // has a reference to a.
      a = new A();
    }
    return a; // Thread 2 returns an instance of a that is not fully                     
              // initialized because (if I understand this right) JLS 
              // does not guarantee that non-final fields are fully 
              // initialized before references get assigned
  }
}
4b9b3361

Ответ 1

То, что вы говорите, верно.

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

Конечное ключевое слово имеет гораздо больше преимуществ. Из Java Concurecncy на практике:

Конечные поля не могут быть изменены (хотя объекты, на которые они ссылаются, могут быть изменены, если они изменяемы), но они     также имеют специальную семантику под моделью памяти Java. Использование конечных полей делает возможной гарантию     безопасности инициализации (см. раздел 3.5.2), которая позволяет беспрепятственно обращаться и делиться неизменяемыми объектами без     синхронизации.

В книгах говорится:

Чтобы безопасно опубликовать объект, как ссылка на объект, так и состояние объекта должны быть видны другим     потоков одновременно. Правильно построенный объект можно безопасно опубликовать:

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

Ответ 2

Я думаю, что на ваш вопрос отвечает JLS прямо под частью, которую вы цитируете, в Раздел 17.5.1: Семантика конечных полей:

Для записи w, замораживания f, действия a (не считанного конечного поля), прочитанного r 1 конечного поля, замороженного f, и чтения r 2, что hb (w, f), hb (f, a), mc (a, r 1) и разыменования (r 1, r 2), то при определении того, какие значения могут быть видны r 2, рассмотрим hb (w, r 2).

Позвольте сломать его в терминах вопроса:

  • w: это запись в bNotFinal по потоку 1
  • f: действие замораживания, которое замерзает b
  • a: опубликовать ссылку на объект A
  • r 1: чтение b (заморожено по f) потоком 2
  • r 2: повтор b.bNotFinal по потоку 2

Заметим, что

  • hb (w, f): запись в bNotFinal происходит до замораживания b
  • hb (f, a): Ссылка A публикуется после завершения конструктора (т.е. после замораживания)
  • mc (a, r 1): Thread 2 читает ссылку A перед чтением A.b
  • dereferences (r 1, r 2): Thread 2 разделяет значение b, обращаясь к b.bNotFinal

Следующее предложение...

", то при определении того, какие значения можно увидеть через r 2, рассмотрим hb (w, r 2)"

... затем переводится на

при определении того, какие значения могут быть видны при чтении b.bNotFinal, мы считаем, что запись в bNotFinal по потоку 1 происходит до чтения b.bNotFinal.

т.е. В строке 2 гарантируется, что значение 2 для b.bNotFinal.


Соответствующая цитата Билла Пью:

Возможность видеть правильно построенное значение для поля хороша, но если само поле является ссылкой, то вы также хотите, чтобы ваш код отображал обновленные значения для объекта (или массива), на который он указывает, Если ваше поле является конечным полем, это также гарантировано. Таким образом, вы можете иметь конечный указатель на массив и не беспокоиться о других потоках, видящих правильные значения для ссылки на массив, а неверные значения для содержимого массива. Опять же, под "правильным" здесь мы подразумеваем "актуальность по состоянию на конец конструктора объекта", а не "последнее доступное значение".

В частности, это прямой ответ на пример @supercat, поднятый в отношении несинхронизированного обмена ссылками String.

Ответ 3

class A {
    private final B b = new B();
}

Вышеприведенная строка гарантирует, что b будет инициализироваться при доступе b из экземпляра A. Теперь деталь b инициализации или любого экземпляра B полностью зависит от того, как определяется B, и в этом случае она может быть инициализирована в соответствии с JLS или не может.

Итак, если вы делаете A a = new A(); из одного потока и каким-то образом удастся прочитать a.b из другого потока, гарантировано, что вы не увидите нуль, если a не является нулевым, но b.bNotFinal может быть все еще нулевым.

Ответ 4

Следует отметить, что final здесь выполняет ту же задачу, что и volatile с точки зрения видимости значений для потоков. Тем не менее, вы не можете использовать как final, так и volatile в поле, поскольку они избыточны друг другу. Вернемся к вашему вопросу. Как и другие, вы указали, что ваше предположение неверно, поскольку JLS гарантирует только видимость ссылки на B, а не неопределенные поля, определенные в B. Однако вы можете заставить B вести себя так, как вы хотите, чтобы он себя вел. Одним из решений является объявление bNotFinal как volatile, если оно не может быть final.

Ответ 5

"Часто задаваемые вопросы по JSR 133 (модель памяти Java), Джереми Мэнсон и Брайан Гетц, февраль 2004 года" описывает, как работает поле final.

Цитата:

Цели JSR 133 включают:

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

Ответ 6

После окончательного присваивания (это забор магазина) есть барьер памяти, так вы гарантируете, что другие потоки будут видеть значение, которое вы назначили. Мне нравится JLS и как он говорит, что делаются с случаями-до/после и гарантией финала, но для меня барьер памяти и его эффекты намного проще понять.

Вы действительно должны прочитать это: Запоминания памяти