Не потокобезопасная публикация объектов

Чтение "Java Concurrency In Practice", в разделе 3.5 есть эта часть:

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

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

Кроме того, для класса Holder, такого как

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

можно бросить AssertionError!

Как это возможно? Единственный способ, который я могу придумать, который может допустить такое нелепое поведение, - это если конструктор Holder не будет блокироваться, поэтому будет создана ссылка на экземпляр, в то время как код конструктора все еще выполняется в другом потоке.

Это возможно?


person Maxim Veksler    schedule 25.10.2009    source источник
comment
Нашел онлайн-ссылку на раздел книги book.javanb.com/ java-concurrency-in-Practice/ch03lev1sec5.html   -  person Maxim Veksler    schedule 25.10.2009
comment
Это означает, что все поля в объекте должны быть окончательными. В любом случае я могу доказать себе, что это может произойти. я пробовал помогите пожалуйста   -  person John    schedule 22.02.2011


Ответы (7)


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

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

Тема 1:

someStaticVariable = new Holder(42);

Тема 2:

someStaticVariable.assertSanity(); // can throw

На первый взгляд кажется невероятным, что это могло когда-либо произойти. Чтобы понять, почему это может произойти, вам нужно преодолеть синтаксис Java и перейти на гораздо более низкий уровень. Если вы посмотрите на код для потока 1, его можно по существу разбить на серию операций записи и выделения памяти:

  1. Выделить память для указателя1
  2. Запишите 42 в pointer1 по смещению 0
  3. Записать указатель1 на someStaticVariable

Поскольку у Java слабая модель памяти, вполне возможно, что код действительно будет выполняться в следующем порядке с точки зрения потока 2:

  1. Выделить память для указателя1
  2. Записать указатель1 на someStaticVariable
  3. Запишите 42 в pointer1 по смещению 0

Страшный? Да, но это может случиться.

Однако это означает, что поток 2 теперь может вызывать assertSanity до того, как n получит значение 42. Значение n может быть прочитано дважды во время assertSanity, один раз до завершения операции № 3 и один раз после, и, следовательно, увидеть два разных значения. и сгенерировать исключение.

ИЗМЕНИТЬ

По словам Джона Скита, AssertionError может по-прежнему возникать с Java 8, если поле не является окончательным.

person JaredPar    schedule 25.10.2009
comment
@Jared: я забыл, что это гарантируется только для конечных полей. IIRC, спецификация CLI ECMA также слаба в этом отношении, но модель памяти .NET делает все операции записи фактически нестабильными. Это только IIRC, хотя :) - person Jon Skeet; 25.10.2009
comment
@Jon, ты прав в отношении .Net и ECMA. Тем не менее, это доставляет интересное удовольствие при просмотре изменений в библиотеках F # и базе кода, поскольку они соответствуют модели ECMA. - person JaredPar; 25.10.2009
comment
Действительно страшно. Я узнал от вашего ответа то, что я понятия не имел о модели памяти Java. Это пугает меня, потому что это может фактически означать, что 90% всех Java-кодов в мире сломаны. Более того, скорость вашего ответа меня поражает. По моим подсчетам, вам потребовалось около 5 минут, чтобы ответить на мой вопрос! Большое спасибо за усилия. - person Maxim Veksler; 25.10.2009
comment
Хорошие парни из Sun объясняют описанные выше отношения Happens Before. java.sun.com/javase/ 6/docs/api/java/util/concurrent/ (#see section Свойства согласованности памяти) - person Maxim Veksler; 02.11.2009
comment
@JaredPar Вы должны удалить свое редактирование - возможно n != n вернуть true в текущей модели памяти (Java 5+). - person assylias; 04.09.2013
comment
Да, пожалуйста, отредактируйте его, чтобы было ясно, что это (обновление?) Только для конечных полей, как сказал @assylias - person Mr_and_Mrs_D; 02.12.2013
comment
В последнем JMM для этого единственного оператора: Holderholder = new Holder() невозможно, чтобы ‹init› выполнялась после astore (ссылка присваивается переменной), верно? Но из-за видимости n != n все еще возможно, верно? - person Chao; 29.08.2016
comment
это действительно для java 6,7,8? - person Dmitry Krivenko; 02.03.2017

Модель памяти Java раньше была такой, что присваивание ссылке Holder могло стать видимым до присваивания переменной внутри объекта.

Однако более поздняя модель памяти, которая вступила в силу с Java 5, делает это невозможным, по крайней мере, для конечных полей: все присваивания в конструкторе «происходят до» любого присваивания ссылки на новый объект переменной. См. спецификацию языка Java, раздел 17.4 для более подробной информации, но вот наиболее подходящий фрагмент:

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

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

Конечно же:

if (n != n)

безусловно, может произойти сбой для неконечных переменных, если JIT-компилятор не оптимизирует его, если операции:

  • Выбрать LHS: n
  • Получить правую сторону: n
  • Сравните левый и правый

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

person Jon Skeet    schedule 25.10.2009
comment
Это означает, что все поля в объекте должны быть окончательными. В любом случае я могу доказать себе, что это может произойти. я пробовал помогите пожалуйста - person John; 22.02.2011
comment
javap показывает, что он не оптимизирует его, поэтому ваше предположение верно: 1: getfield # 2 4: aload_0 5: getfield # 2 8: if_icmpeq 21 - person Eran Medan; 08.06.2013
comment
То, что вы цитируете, касается окончательной семантики. Но для этого утверждения: Holderholder = new Holder(), последняя версия JMM может гарантировать, что ‹init› будет выполнен до astore? не могли бы вы сказать мне раздел в JLS. - person Chao; 29.08.2016
comment
@Richard: Боюсь, совсем не уверен. - person Jon Skeet; 29.08.2016
comment
@JonSkeet Это означает, что частично инициализированный объект может быть виден другому потоку, если синхронизация не применяется. - person Chao; 30.08.2016
comment
@Richard: Ну, это не обязательно означает это - это просто означает, что в настоящее время я не могу доказать, что это невозможно. Помимо всего прочего, прошлой ночью у меня не было времени гоняться за ссылками на JLS. Я могу увидеть, если я могу получить время позже. (Но я подозреваю, что вы можете видеть частично инициализированные объекты с точки зрения неконечных полей, но не финальных полей.) - person Jon Skeet; 30.08.2016

Ну, в книге для первого блока кода указано, что:

Проблема здесь не в самом классе Holder, а в том, что Holder не опубликован должным образом. Тем не менее, Holder можно сделать невосприимчивым к неправильной публикации, объявив поле n окончательным, что сделало бы Holder неизменяемым; см. раздел 3.5.2

И для второго блока кода:

Поскольку синхронизация не использовалась для того, чтобы сделать держатель видимым для других потоков, мы говорим, что держатель не был должным образом опубликован. Две вещи могут пойти не так с неправильно опубликованными объектами. Другие потоки могут увидеть устаревшее значение для поля держателя и, таким образом, увидеть нулевую ссылку или другое старое значение, даже если значение было помещено в держатель. Но, что еще хуже, другие потоки могли видеть актуальное значение для ссылки на держатель, но устаревшие значения для состояния держателя.[16] Чтобы сделать вещи еще менее предсказуемыми, поток может увидеть устаревшее значение при первом чтении поля, а затем более актуальное значение в следующий раз, поэтому assertSanity может выдать AssertionError.

Я думаю, что JaredPar в значительной степени сделал это явным в своем комментарии.

(Примечание: здесь не нужно искать голоса — ответы позволяют получить более подробную информацию, чем комментарии.)

person alphazero    schedule 25.10.2009

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

a = 1;
b = 2;

Если вы сделаете это в одном потоке, второй поток может увидеть, что для b установлено значение 2, прежде чем для a будет установлено значение 1. Кроме того, может быть неограниченное количество времени между вторым потоком, увидевшим, что одна из этих переменных обновляется, и другая переменная обновляется.

person R Samuel Klatchko    schedule 25.10.2009

если посмотреть на это с точки зрения здравого смысла, если предположить, что утверждение

if(n != n)

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

person twolfe18    schedule 25.10.2009
comment
Кто-нибудь хочет объяснить -1? - person twolfe18; 01.11.2009
comment
Не минусовал, но ваше предположение, что n !=n неверно. - person Matthias Braun; 06.11.2020

Этот пример относится к категории "Ссылка на объект, содержащий конечное поле, не вышла из конструктора"

Когда вы создаете экземпляр нового объекта Holder с помощью нового оператора,

  1. виртуальная машина Java сначала выделит (как минимум) достаточно места в куче для хранения всех переменных экземпляра, объявленных в Holder и его суперклассах.
  2. Во-вторых, виртуальная машина инициализирует все переменные экземпляра начальными значениями по умолчанию. 3.c В-третьих, виртуальная машина вызовет метод класса Holder.

см. выше: http://www.artima.com/designtechniques/initializationP.html

Предположим: 1-й поток начинается в 10:00, он вызывает инстатированный объект держателя, вызывая новый Holer (42), 1) виртуальная машина Java сначала выделит (по крайней мере) достаточно места в куче для хранения всего экземпляра переменные, объявленные в Holder и его суперклассах. -- это произойдет в 10:01 2) во-вторых, виртуальная машина инициализирует все переменные экземпляра их начальными значениями по умолчанию -- она ​​начнется в 10:02 3) в-третьих, виртуальная машина вызовет метод в классе Holder .-- начнется в 10:04

Теперь Thread2 начался в --> 10:02:01, и он вызовет assertSanity() в 10:03, к тому времени n был инициализирован с нулевым значением по умолчанию, второй поток считывает устаревшие данные.

// небезопасная публикация public Holderholder;

если вы опубликуете окончательный держатель, держатель решит эту проблему

or

частный интервал n; если вы сделаете закрытый финал int n; решит этот вопрос.

см.: http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html в разделе Как финальные поля работают в новом JMM?

person rameshvanka    schedule 17.01.2014

Меня тоже очень озадачил этот пример. Я нашел веб-сайт, который подробно объясняет тему и может быть полезен читателям: https://www.securecoding.cert.org/confluence/display/java/TSM03-J.+Do+not+publish+partially+инициализированные+объекты

Изменить: соответствующий текст из ссылки гласит:

JMM позволяет компиляторам выделять память для нового объекта Helper и присваивать ссылку на эту память вспомогательному полю перед инициализацией нового объекта Helper. Другими словами, компилятор может переупорядочить запись в поле экземпляра помощника и запись, которая инициализирует объект помощника (т. е. this.n = n), так что первое происходит первым. Это может открыть окно гонки, в течение которого другие потоки могут наблюдать за частично инициализированным экземпляром объекта Helper.

person user3599526    schedule 28.02.2016