Вопрос

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

Возможно ли это?

Это было полезно?

Решение

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

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

Резьба 1:

someStaticVariable = new Holder(42);

Поток 2:

someStaticVariable.assertSanity(); // can throw

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

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

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

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

Пугающий?Да, но это может случиться.

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

Редактировать

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

Другие советы

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

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

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

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

Конечно, тот:

if (n != n)

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

  • Извлекать LHS:n
  • Извлекать RHS:n
  • Сравните LHS и RHS

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

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

  

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

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

  

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

Я думаю, что JaredPar сделал это в своем комментарии.

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

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

a = 1;
b = 2;

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

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

if (n! = n)

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

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

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

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

пожалуйста, обратитесь к приведенному выше: http://www.artima.com/designtechniques/initializationP.html

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

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

// небезопасная публикация публичный правообладатель;

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

или

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

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

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

Редактировать:Соответствующий текст по ссылке гласит:

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top