Почему такое поведение допускается в модели памяти 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
, либо мы только что выполнили запись 42 в x
, или мы только что прочитали x
и увидели значение 42. В любом случае это будет легальным для чтения x
, чтобы увидеть значение 42. Затем он может изменить r1 = x
на r1 = 42
; это позволило бы преобразовать y = r1
в y = 42
и выполнить ранее, в результате чего поведение в вопросе. В этом случае запись в y
выполняется первый.
Мой вопрос в том, что это за оптимизация компилятора? (Я компилятор-невежда.) Поскольку 42 записывается только условно, когда оператор if
выполняется, как компилятор может решить записать с помощью x
?
Во-вторых, даже если компилятор выполняет эту спекулятивную оптимизацию и совершает y = 42
и
то, наконец, делает r3 = 42
, не является ли это нарушением цикла причинности, так как теперь нет разницы в причинах и следствиях?
На самом деле есть один пример в том же документе (стр. 15, рис. 7), где подобный причинный цикл упоминается как неприемлемый.
Итак, почему этот порядок выполнения является законным в JMM?
Ответы
Ответ 1
Как объяснено, единственными значениями, когда-либо записанными в 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
. Дело в том, что Thread 1 будет всегда, безоговорочно писать 42 на y
. Переменная r3
на самом деле избыточна и может быть полностью исключена из машинного кода. Таким образом, код в примере приводит только к появлению причинной стрелки от x
до y
, но подробный анализ показывает, что на самом деле нет причинности. Удивительным последствием является то, что запись в y
может быть выполнена раньше.
Общее замечание об оптимизации: я полагаю, вы знакомы с оценками производительности, связанными с чтением из основной памяти. Вот почему компилятор JIT склонен отказываться делать это, когда это возможно, и в этом примере выясняется, что на самом деле не нужно читать x
, чтобы знать, что писать на y
.
Общее примечание к обозначениям: r1
, r2
, r3
являются локальными переменными (они могут быть в стеке или в регистре CPU); x
, y
являются разделяемыми переменными (они находятся в основной памяти). Не принимая во внимание это, примеры не будут иметь смысла.
Ответ 2
Компилятор может выполнять некоторые анализы и оптимизации и заканчивать следующим кодом для Thread1:
y=42; // step 1
r3=x; // step 2
x=42; // step 3
Для однопоточного исполнения этот код эквивалентен исходному коду и поэтому является законным. Затем, если код Thread2 выполняется между шагами 1 и шагом 2 (что вполне возможно), тогда r3 также назначается 42.
Вся идея этого примера кода заключается в демонстрации необходимости правильной синхронизации.
Ответ 3
Его ничего не стоит, что javac
не в значительной степени оптимизирует код. JIT оптимизирует код, но довольно консервативен в отношении кода переупорядочения. ЦП может переупорядочить выполнение, и он делает это в малой степени.
Принуждение CPU к тому, чтобы оптимизировать уровень инструкций, довольно дорого, например. он может замедлить его в 10 раз или более. AFAIK, разработчики Java хотели указать минимальные требуемые гарантии, которые будут эффективно работать на большинстве процессоров.