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

Непотенциальное размещение объектов

Чтение Java concurrency на практике, раздел 3.5: Требование поднято, что

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

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

4b9b3361

Ответ 1

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

Тема 1:

someStaticVariable = new Holder(42);

Тема 2:

someStaticVariable.assertSanity(); // can throw

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

  • Память Alloc для указателя1
  • Напишите 42 в указатель1 при смещении 0
  • Введите указатель1 в someStaticVariable

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

  • Память Alloc для указателя1
  • Введите указатель1 в someStaticVariable
  • Напишите 42 в указатель1 при смещении 0

Страшно? Да, но это может произойти.

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

ИЗМЕНИТЬ

По словам Джона, это невозможно (к счастью) с новыми версиями Java из-за обновлений модели памяти.

EDIT 2nd

По словам Джона, он никогда не говорит, что это невозможно с 8-й версией Java, если поле не является окончательным.

Ответ 2

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

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

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

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

Конечно:

if (n != n)

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

  • Fetch LHS: n
  • Fetch RHS: n
  • Сравнить LHS и RHS

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

Ответ 3

Хорошо, в книге он указывает на первый блок кода, что:

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

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

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

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

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

Ответ 4

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

a = 1;
b = 2;

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

Ответ 5

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

if(n != n)

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

Ответ 6

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

Когда вы создаете новый объект Holder с новым оператором,

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

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

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

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

//небезопасная публикация владелец общественного держателя;

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

или

private int n; если вы делаете закрытый окончательный int n; будет устранена эта проблема.

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

Ответ 7

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

Изменить: В соответствующем тексте из ссылки говорится:

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