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

ВАЖНОЕ ИЗМЕНЕНИЕ Я знаю, что "происходит до" в потоке, где выполняются два назначения, мой вопрос: возможно ли, что другой поток будет читать "b", не равный нулю, a "по-прежнему является нулевым. Поэтому я знаю, что если вы вызываете doIt() из того же потока, что и тот, где вы ранее называли setBothNonNull (...), тогда он не может вызывать исключение NullPointerException. Но что, если вы вызываете doIt() из другого потока, чем тот, который вызывает setBothNonNull (...)?

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

Скажем, у нас есть объект, содержащий две ссылки volatile 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", я прочитаю ненулевой "a" ?

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

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

Ответ 1

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

a = one;
b = two;

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

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

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

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

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

Ответ 2

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

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

  • By (1) - hb (write (a, non-null), write (b, non-null)) в потоке # 1
  • By (2) и ( 4) - hb (write (b, non-null), read (b, non-null))
  • By (1) - hb (read (b, non-null), read (a, XXX)) в потоке # 2,
  • By (4) - hb (write (a, non-null), read (b, non-null))
  • By (4) - hb (write (a, non-null), read (a, XXX))

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

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

Ответ 3

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

Ответ 4

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

Ответ 5

Я прочитал эту страницу и нашел нелетучую и несинхронизированную версию вашего вопроса:

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.

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