Неустойчивые гарантии и исполнение вне очереди

ВАЖНОЕ РЕДАКТИРОВАНИЕ. Я знаю о "происходит до" в потоке, в котором выполняются два назначения, мой вопрос: возможно ли это для другого потока читать «b» не равным нулю, пока «а» все еще равно нулю. Итак, я знаю, что если вы вызываете doIt () из того же потока, что и тот, где вы ранее вызывали setBothNonNull (...), тогда он не может вызвать исключение NullPointerException. Но что, если один вызывает doIt () из другого потока, чем тот, который вызывает setBothNonNull (...)?

Обратите внимание, что этот вопрос касается исключительно ключевого слова volatile и volatile гарантий: он не относится к ключевому слову synchronized (поэтому, пожалуйста, не отвечайте «вы должны использовать синхронизацию», потому что у меня нет никаких проблем для решения: я просто хочу понять volatile гарантии (или отсутствие гарантий) в отношении исполнения вне очереди).

Скажем, у нас есть объект, содержащий две ссылки volatile String, которые инициализируются конструктором значением null, и что у нас есть только один способ изменить эти две String: вызовом setBoth (...) и что мы можем только впоследствии устанавливайте свои ссылки на ненулевые ссылки (только конструктору разрешено устанавливать их в null).

Например (это всего лишь пример, вопросов пока нет):

public class SO {

    private volatile String a;
    private volatile String b;

    public SO() {
        a = null;
        b = null;
    }

    public void setBothNonNull( @NotNull final String one, @NotNull final String two ) {
        a = one;
        b = two;
    }

    public String getA() {
        return a;
    }

    public String getB() {
        return b;
    }

}

В setBothNoNull (...) строка, назначающая ненулевой параметр «a», появляется перед строкой, назначающей ненулевой параметр «b».

Затем, если я сделаю это (опять же, нет вопросов, следующий вопрос):

doIt() {
    if ( so.getB() != null ) {
        System.out.println( so.getA().length );
    }
}

Правильно ли я понимаю, что из-за неупорядоченного выполнения я могу получить NullPointerException?

Другими словами: нет никакой гарантии, что, поскольку я прочитал ненулевое значение «b», я прочту ненулевое значение «а»?

Потому что из-за неисправного (мульти) процессора и способа работы volatile "b" может быть поставлено перед "a"?

volatile гарантирует, что при чтении после записи всегда будет отображаться последнее записанное значение, но здесь есть "проблема" вне очереди, верно? (еще раз, «проблема» сделана с целью попытаться понять семантику ключевого слова volatile и модели памяти Java, а не для решения проблемы).


person SyntaxT3rr0r    schedule 14.03.2010    source источник
comment
Если нет гарантии, что a будет назначен до b, тогда возможно получить ненулевое b и все равно получить нулевое a. Какие гарантии есть с volatile? Действительно хороший вопрос +1.   -  person kiwicptn    schedule 14.03.2010


Ответы (5)


Нет, вы никогда не получите NPE. Это потому, что volatile также имеет эффект памяти введения отношения «произошло до». Другими словами, это предотвратит переупорядочивание

a = one;
b = two;

Приведенные выше операторы не будут переупорядочены, и все потоки будут наблюдать значение one для a, если b уже имеет значение two.

Вот ветка, в которой Дэвид Холмс объясняет это:
http://markmail.org/message/j7omtqqh6ypwshfv#query:+page:1+mid:34dnnukruu23ywzy+state:results

РЕДАКТИРОВАТЬ (ответ на продолжение): Холмс говорит, что компилятор мог бы теоретически выполнить переупорядочение, если бы был только поток A. Однако есть другие потоки, и они МОГУТ обнаружить переупорядочивание. Вот почему компилятору НЕ разрешается делать такое переупорядочение. Модель памяти Java требует, чтобы компилятор специально удостоверился, что ни один поток никогда не обнаружит такое переупорядочение.

Но что, если кто-то вызывает doIt () из другого потока, чем тот, который вызывает setBothNonNull (...)?

Нет, у вас НИКОГДА не будет НПЭ. volatile семантика требует упорядочивания между потоками. Это означает, что для всего существующего потока присвоение one происходит до назначения two.

person Enno Shioji    schedule 14.03.2010
comment
То, что написал Дэвид Холмс, еще более сбивает с толку, поскольку в нем говорится: Как вы говорите, вызывающий поток methodA () не может сказать, переупорядочивается ли что-нибудь внутри methodA (), но другой поток может это сказать. как другой поток может сказать, что произошел переупорядочение, если в моем примере я не могу прочитать b, отличное от нуля, и нулевое значение? (в моем примере это был бы единственный способ обнаружить, что произошел переупорядочение). - person SyntaxT3rr0r; 14.03.2010
comment
Привет, я добавил свой ответ к исходному ответу. - person Enno Shioji; 15.03.2010
comment
steinen: Большое спасибо за продолжение, я приму ваш ответ, но сначала я оставлю комментарии / ответ других людей, если кто-то хочет что-то добавить, большое спасибо за то, что нашли время, чтобы объяснить это мне :) - person SyntaxT3rr0r; 15.03.2010
comment
@WizardOfOdds: NP :) В будущем вы можете направлять свои вопросы по адресу altair.cs.oswego.edu/mailman/listinfo/concurrency-interest Это список рассылки, куда часто заходят люди JSR166, они очень дружелюбны и полезны. И, конечно же, они знают толк в параллелизме java буквально наизнанку. - person Enno Shioji; 15.03.2010

Правильно ли я понимаю, что из-за неупорядоченного выполнения я могу получить исключение NullPointerException? Другими словами: нет гарантии, что, поскольку я прочитал ненулевое значение b, я прочту ненулевое значение a?

Предполагая, что значения, присвоенные a и b, или ненулевые, я думаю, что ваше понимание неверно. JLS говорит следующее:

(1) Если x и y являются действиями одного и того же потока и x стоит перед y в программном порядке, тогда hb (x, y).

(2) Если действие x синхронизируется со следующим действием y, то у нас также есть hb (x, y).

(3) Если hb (x, y) и hb (y, z), то hb (x, z).

и

(4) Запись в изменчивую переменную (§8.3.1.4) v синхронизируется - со всеми последующими чтениями v любым потоком (где последующее определяется в соответствии с синхронизацией приказ).

Теорема

Учитывая, что поток №1 вызывал setBoth(...); один раз, и что аргументы были ненулевыми, и этот поток №2 наблюдал, что b не является нулевым, тогда поток №2 не может наблюдать, что a имеет значение NULL.

Неофициальное доказательство

  1. By (1) - hb (запись (a, ненулевое значение), запись (b, ненулевое значение)) в потоке №1
  2. По (2) и (4) - hb (запись (b, ненулевое значение), чтение (b, ненулевое значение))
  3. By (1) - hb (читать (b, ненулевое), читать (a, XXX)) в потоке №2,
  4. By (4) - hb (запись (a, ненулевое значение), чтение (b, ненулевое значение))
  5. По (4) - hb (написать (a, не ноль), прочитать (a, XXX))

Другими словами, запись ненулевого значения в a происходит до чтения значения (XXX) a. Единственный способ, которым XXX может быть нулевым, - это если было какое-то другое действие, записывающее null в a, такое, что hb (write (a, non-null), write (a, XXX)) и hb (write (a, XXX), читать (а, ХХХ)). А это невозможно согласно определению проблемы, и поэтому XXX не может быть нулевым. QED.

Объяснение - JLS заявляет, что отношение hb (...) (происходит раньше) не запрещает полностью переупорядочивание. Однако, если hb (xx, yy), то изменение порядка действий xx и yy разрешено только, если результирующий код имеет тот же наблюдаемый эффект, что и исходная последовательность.

person Stephen C    schedule 14.03.2010
comment
@Stephen C: я должен был назвать его setBothNonNull, но установка первого аргумента на null не может произойти, потому что @NotNull гарантированно вызовет исключение (пользователь IntelliJ IDEA уже много лет, и я начал использовать @NotNull, как только он был доступен для IntelliJ задолго до того, как он появился в Eclipse). - person SyntaxT3rr0r; 14.03.2010
comment
@Stephen C: Если x и y являются действиями одного и того же потока ..., но мой вопрос заключается в том, читает ли другой поток летучие компоненты. Читая ваш ответ, вы говорите, что выполнение вне очереди - это несуществующая концепция в Java, что я не думаю, что это правда. Если бы я выполнял свой ненулевой / нулевой тест из того же потока, в котором я выполняю задание, тогда я знаю, что у меня не может быть NPE. У меня вопрос: а что, если я провожу этот тест из другой ветки? - person SyntaxT3rr0r; 14.03.2010
comment
@Stephen C: +1 ... Мне очень нравится очень аккуратное редактирование вашего вопроса. Мне просто нужно разобраться в мельчайших подробностях того, что случилось раньше. - person SyntaxT3rr0r; 16.03.2010

Я нашел следующий пост, в котором объясняется, что volatile имеет ту же семантику упорядочения, что и synchronized в этом случае. Java Volatile - это мощный инструмент

person Brandon Bodnar    schedule 14.03.2010
comment
@Brandon Bodnar: спасибо за эту ссылку ... К сожалению, статья не объясняет многое о повторном заказе. В нем говорится, что переупорядочение гарантируется в уникальном потоке, но далее приводится пример, где threadTwo () может читать b true и false. Это точно так же, как мой пример с ненулевым / нулевым значением. Совершенно непонятно, является ли это просто примером, и исправляет ли это volatile или нет. - person SyntaxT3rr0r; 14.03.2010

Хотя ответы Стивена С и принятые ответы хороши и в значительной степени охватывают это, стоит сделать важное замечание, что переменная a не обязательно должна быть изменчивой - и вы все равно не получите NPE. Это связано с тем, что между a = one и b = two будет связь «произошло раньше», независимо от того, является ли a volatile. Таким образом, формальное доказательство Стивена Си по-прежнему применимо, просто нет необходимости в том, чтобы a был изменчивым.

person almondandapricot    schedule 04.03.2013

Я прочитал эту страницу и обнаружил, что -Энергетическая и несинхронизированная версия вашего вопроса:

class Simple {
    int a = 1, b = 2;
    void to() {
        a = 3;
        b = 4;
    }
    void fro() {
        System.out.println("a= " + a + ", b=" + b);
    }
}

fro может получить 1 или 3 для значения a и независимо может получить 2 или 4 для значения b.

(Я понимаю, что это не отвечает на ваш вопрос, но дополняет его.)

person kiwicptn    schedule 14.03.2010
comment
Обратите внимание, что цитируемая вами страница взята из второго издания Спецификации языка Java, которое с тех пор было заменено третьим изданием, в котором больше нет цитируемого вами раздела. - person Avi; 14.03.2010
comment
@kiwicptn: спасибо за да, но, как написал Ави, с тех пор модель памяти Java изменилась (поэтому теперь двойная проверка блокировки работает, например, если вы используете изменчивую память, а раньше этого не было). - person SyntaxT3rr0r; 14.03.2010