Почему такое поведение разрешено в модели памяти Java?
Причинность в JMM, кажется, самая запутанная часть этого. У меня есть несколько вопросов, касающихся причинности JMM и разрешенного поведения в параллельных программах.
Как я понимаю, текущий JMM всегда запрещает причинно-следственные связи. (Я прав?)
Теперь, согласно документу JSR-133, стр. 24, рис.16, у нас есть пример, где:
Первоначально x = y = 0
Тема 1:
r3 = x;
if (r3 == 0)
x = 42;
r1 = x;
y = r1;
Тема 2:
r2 = y;
x = r2;
Наглядно, r1 = r2 = r3 = 42
кажется невозможным. Тем не менее, это упоминается не только как возможно, но и "разрешено" в JMM.
Для возможности объяснение из документа, который я не понимаю:
Компилятор может определить, что единственные значения, когда-либо назначенные
x
0 и 42. Исходя из этого, компилятор может вывести это в точке, где мы выполняемr1 = x
Либо мы только что выполнили запись 42x
или мы только что прочиталиx
и видел значение 42. В любом случае это было бы законно для чтенияx
чтобы увидеть значение 42. Затем он может изменитьr1 = x
вr1 = 42
; это позволило быy = r1
быть преобразованным вy = 42
и выполнено ранее, что привело к соответствующему поведению. В этом случае пишитеy
совершается первым.
У меня вопрос, что это за оптимизация компилятора? (Я не знаю компилятора.) Поскольку 42 пишется только условно, когда if
Утверждение выполнено, как компилятор может решить написать x
?
Во-вторых, даже если компилятор делает эту спекулятивную оптимизацию и фиксирует y = 42
а потом наконец делает r3 = 42
не является ли это нарушением причинно-следственной связи, поскольку различий между причинами и следствиями не осталось?
Фактически, в одном и том же документе (стр. 15, рис. 7) есть один пример, где подобный причинный цикл упоминается как неприемлемый.
Так почему же этот порядок исполнения является законным в JMM?
3 ответа
Как объяснено, единственные значения, когда-либо написанные x
0 и 42. Тема 1:
r3 = x; // here we read either 0 or 42
if (r3 == 0)
x = 42;
// at this point x is definitely 42
r1 = x;
Поэтому JIT-компилятор может переписать r1 = x
как r1 = 42
, и далее y = 42
, Дело в том, что поток 1 всегда безоговорочно записывает 42 y
, r3
переменная на самом деле является избыточной и может быть полностью исключена из машинного кода. Таким образом, код в примере только дает вид причинной стрелки из x
в y
Детальный анализ показывает, что причинно-следственной связи нет. Удивительным последствием является то, что y
может быть совершено рано.
Общее замечание по оптимизации: я так понимаю, вы знакомы с потерями производительности, возникающими при чтении из основной памяти. Вот почему JIT-компилятор склонен отказываться делать это всякий раз, когда это возможно, и в этом примере оказывается, что на самом деле ему не нужно читать x
чтобы знать что писать y
,
Общее примечание к обозначениям: r1
, r2
, r3
являются локальными переменными (они могут быть в стеке или в регистрах ЦП); x
, y
являются общими переменными (они находятся в основной памяти). Без учета этого примеры не будут иметь смысла.
Компилятор может выполнить некоторые анализы и оптимизации и закончить следующим кодом для Thread1:
y=42; // step 1
r3=x; // step 2
x=42; // step 3
Для однопоточного исполнения этот код эквивалентен исходному коду и поэтому является законным. Затем, если код Thread2 выполняется между этапом 1 и этапом 2 (что вполне возможно), тогда r3 также назначается 42.
Вся идея этого примера кода состоит в том, чтобы продемонстрировать необходимость правильной синхронизации.
Это ничего не стоит, что javac
не оптимизирует код в значительной степени. JIT оптимизирует код, но довольно консервативен в отношении переупорядочения кода. Процессор может переупорядочить выполнение, и это делает это в небольшой степени довольно много.
Заставить ЦП не выполнять оптимизацию на уровне команд довольно дорого, например, это может замедлить его в 10 и более раз. AFAIK, разработчики Java хотели указать необходимый минимум гарантий, который бы эффективно работал на большинстве процессоров.