Волатильные гарантии и исполнение заказа
ВАЖНОЕ РЕДАКТИРОВАНИЕ Я знаю о том, что "происходит раньше" в потоке, в котором происходят два назначения, мой вопрос: возможно ли, чтобы другой поток читал "b" не ноль, в то время как "a" все еще нуль. Поэтому я знаю, что если вы вызываете doIt() из того же потока, в котором вы ранее вызывали setBothNonNull (...), он не может вызвать исключение NullPointerException. Но что, если вы вызываете doIt() из другого потока, а не из вызывающего setBothNonNull (...)?
Обратите внимание, что этот вопрос касается только volatile
Ключевое слово и volatile
гарантии: речь идет не о synchronized
ключевое слово (поэтому, пожалуйста, не отвечайте "вы должны использовать синхронизацию", потому что у меня нет проблем для решения: я просто хочу понять volatile
гарантии (или отсутствие гарантий) в отношении исполнения заказа.
Скажем, у нас есть объект, содержащий два volatile
Строковые ссылки, которые инициализируются конструктором в null, и что у нас есть только один способ изменить две строки: вызывая setBoth (...), и что мы можем только потом установить их ссылки на ненулевую ссылку (только конструктор разрешено устанавливать их на ноль).
Например (это просто пример, вопросов пока нет):
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
Работы "б" можно было бы присвоить раньше "а"?
volatile
Гарантии, что чтения после записи всегда будут видеть последнее записанное значение, но здесь есть неупорядоченная "проблема", верно? (еще раз, "проблема" делается с целью попытаться понять семантику volatile
ключевое слово и модель памяти Java, чтобы не решить проблему).
5 ответов
Нет, вы никогда не получите NPE. Это потому что volatile
также имеет эффект памяти введения отношений "до того как случится". Другими словами, это предотвратит изменение порядка
a = one;
b = two;
Приведенные выше утверждения не будут переупорядочены, и все потоки будут соблюдать значение one
за a
если b
уже имеет значение two
,
Вот нить, в которой Дэвид Холмс объясняет это:
http://markmail.org/message/j7omtqqh6ypwshfv
РЕДАКТИРОВАТЬ (ответ на продолжение): Холмс говорит, что теоретически компилятор мог бы сделать переупорядочение, если бы был только поток А. Однако существуют другие потоки, и они МОГУТ обнаружить переупорядочение. Вот почему компилятору НЕ разрешено делать это переупорядочение. Модель памяти Java требует, чтобы компилятор определенно удостоверился, что никакой поток никогда не обнаружит такое переупорядочение.
Но что, если вы вызываете doIt() из другого потока, а не из вызывающего setBothNonNull(...)?
Нет, вы все равно никогда не будете иметь NPE. volatile
семантика навязывает порядок между потоками. Это означает, что для всех существующих потоков присваивание one
происходит до назначения two
,
Правильно ли я понимаю, что из-за неправильного выполнения я могу получить исключение 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
быть нулевым.
Неофициальное доказательство
- По (1) - hb (запись (a, не ноль), запись (b, не ноль)) в потоке #1
- По (2) и (4) - hb (запись (b, не нуль), чтение (b, не ноль))
- По (1) - hb (читай (b, не ноль), читай (a, XXX)) в теме #2,
- По (4) - hb (запись (a, не нуль), чтение (b, не ноль))
- По (4) - hb (записать (a, не ноль), прочитать (a, XXX))
Другими словами, запись ненулевого значения в a
"происходит до" чтения значения (XXX) a
, Единственный способ, которым XXX может быть нулем, - это если какое-то другое действие записывает ноль в a
такой, что hb (запись (a, ненулевое значение), запись (a, XXX)) и hb (запись (a, XXX), чтение (a, XXX)). И это невозможно в соответствии с определением проблемы, и поэтому XXX не может быть нулевым. QED.
Объяснение - JLS утверждает, что отношение hb(...) ("происходит до") не полностью запрещает переупорядочение. Однако если hb(xx,yy), то изменение порядка действий xx и yy разрешено только в том случае, если результирующий код имеет тот же наблюдаемый эффект, что и исходная последовательность.
Хотя Стивен С и принятые ответы хороши и в значительной степени покрывают его, стоит отметить, что переменная а не должна быть изменчивой - и вы все равно не получите NPE. Это связано с тем, что между a = one
а также b = two
, несмотря на погоду a
является volatile
, Таким образом, формальное доказательство Стивена С. все еще применяется, просто не нужно a
быть изменчивым
Я нашел следующий пост, который объясняет, что volatile имеет ту же семантику упорядочения, что и синхронизированная в этом случае. Java Volatile - это мощно
Я прочитал эту страницу и нашел энергонезависимую и несинхронизированную версию вашего вопроса:
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
,
(Я понимаю, что это не отвечает на ваш вопрос, но дополняет его.)