Не поточно-ориентированная публикация объектов - PullRequest
14 голосов
/ 25 октября 2009

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

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

Ответы [ 7 ]

14 голосов
/ 25 октября 2009

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

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

Тема 1:

someStaticVariable = new Holder(42);

Тема 2:

someStaticVariable.assertSanity(); // can throw

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

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

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

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

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

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

EDIT

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

10 голосов
/ 25 октября 2009

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

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

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

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

Конечно же:

if (n != n)

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

  • Получить LHS: n
  • Получить RHS: n
  • Сравнить LHS и RHS

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

1 голос
/ 25 октября 2009

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

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

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

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

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

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

0 голосов
/ 29 февраля 2016

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

Edit: В соответствующем тексте по ссылке написано:

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

0 голосов
/ 17 января 2014

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

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

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

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

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

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

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

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

или

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

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

0 голосов
/ 25 октября 2009

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

a = 1;
b = 2;

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

0 голосов
/ 25 октября 2009

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

if(n != n)

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

...