Почему такое поведение допускается в модели памяти Java? - программирование
Подтвердить что ты не робот

Почему такое поведение допускается в модели памяти 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?

4b9b3361

Ответ 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 хотели указать минимальные требуемые гарантии, которые будут эффективно работать на большинстве процессоров.