Переупорядочение заданий и добавление забора
Следующий код Java выглядит немного странным, потому что я упростил его до простых вещей. Я думаю, что у кода есть проблема с упорядочением. Я смотрю на первую таблицу в JSR-133 Cookbook, и кажется, что нормальный магазин можно переупорядочить с помощью энергозависимого хранилища в change()
.
Может ли присвоение m_normal
в change()
двигаться впереди назначения m_volatile
? Другими словами, может get()
вернуть null
?
Каков наилучший способ решить эту проблему?
private Object m_normal = new Object();
private volatile Object m_volatile;
public void change() {
Object normal;
normal = m_normal; // Must capture value to avoid double-read
if (normal == null) {
return;
}
m_volatile = normal;
m_normal = null;
}
public Object get() {
Object normal;
normal = m_normal; // Must capture value to avoid double-read
if (normal != null) {
return normal;
}
return m_volatile;
}
Примечание. Я не контролирую код, в котором объявлен m_normal
.
Примечание. Я запускаюсь на Java 8.
Ответы
Ответ 1
TL; DR: Друзья не позволяют друзьям тратить время на то, чтобы выяснить, работает ли расистский доступ до экстремальных Concurrency желаний оптимиста. Используйте volatile
и спать счастливо.
Я смотрю на первую таблицу в кулинарной книге JSR-133
Обратите внимание, что полное название "JMM Cookbook For Compiler Writers". Который задает вопрос: являются ли мы компилятором здесь или просто пользователями, пытающимися выяснить наш код? Я думаю, что последнее, поэтому мы должны действительно закрыть JKM Cookbook и открыть сам JLS. См. "Миф: JSR 133 Cookbook - это JMM Synopsis" и раздел после этого.
Другими словами, может() возвращать null?
Да, тривиально get()
, соблюдая значения по умолчанию для полей, не наблюдая ничего, что change()
.:)
Но я предполагаю, что вопрос: разрешено ли видеть старое значение в m_volatile
после завершения change()
(Caveat: для некоторого понятия "завершено", потому что это означает время, а логическое время задается JMM).
Вопрос в основном, есть ли допустимое выполнение, включающее read(m_normal):null --po/hb--> read(m_volatile):null
, с чтением m_normal
, наблюдающим запись null
в m_normal
? Да, вот он: write(m_volatile, X) --po/hb--> write(m_normal, null) ... read(m_normal):null --po/hb--> read(m_volatile):null
.
Чтение и запись в m_normal
не упорядочены, поэтому структурных ограничений, запрещающих выполнение, которое читает оба значения нуля, не существует. Но "неустойчивый", вы бы сказали! Да, это связано с некоторыми ограничениями, но в неправильном порядке w.r.t. энергонезависимые операции, см. "Pitfall: получение и освобождение в неправильном порядке" (посмотрите на этот пример, он очень похож на то, что вы спрашивают).
Верно, что операции над m_volatile
сами предоставляют некоторую семантику памяти: запись в m_volatile
является "выпуском", которая "публикует" все, что произошло до нее, а чтение из m_volatile
- это "приобретать" "что" все "публикуется. Если вы точно делаете вывод, как в этом посте, появляется шаблон: вы можете тривиально перемещать операции над программой" release" вверх (в любом случае это было здорово!), И вы можете тривиально перемещать операции над программой "приобретать" -друг (это было так же здорово!).
Эта интерпретация часто называется "roach motel semantics" и дает интуитивный ответ: "Могут ли эти два утверждения переупорядочиваться?"
m_volatile = value; // release
m_normal = null; // some other store
Ответ под семантикой roach motel "да".
Каков наилучший способ решить эту проблему?
Лучший способ решить - избегать ярких операций для начала и, таким образом, избежать всего беспорядка. Просто сделайте m_normal
volatile
, и все вы настроены: операции над m_normal
и m_volatile
будут последовательно согласованы.
Добавил бы value = m_volatile; после m_volatile = значение; предотвратить назначение m_normal до назначения m_volatile?
Итак, вопрос в том, поможет ли это:
m_volatile = value; // "release"
value = m_volatile; // poison "acquire" read
m_normal = null; // some other store
В наивном мире только семантики мохнатого мохната, это могло бы помочь: казалось бы, что яд приобретает, нарушает движение кода. Но, поскольку значение этого чтения ненаблюдается, оно эквивалентно исполнению без явного чтения, и хорошие оптимизаторы будут использовать это. См. "Желательное мышление: ненаблюдаемые летучие имеют эффекты памяти" . Важно понимать, что летучие не всегда означают барьеры, даже если консервативная реализация, изложенная в JMM Cookbook для компиляторов, имеет их.
Кроме того, существует альтернатива VarHandle.fullFence()
, которая может использоваться в примере, подобном этому, но она ограничена очень мощными пользователями, потому что рассуждение с барьерами становится пограничным безумным. См. "Миф: Барьеры - это разумная ментальная модель" и "Миф: переупорядочение И Commit to Memory" .
Просто сделайте m_normal
volatile
, и все будут спать лучше.
Ответ 2
// Must capture value to avoid double-read
Богохульство. Компилятор имеет право делать то, что ему нравится, с обычными обращениями, повторяя их, когда нет кода Java, выполняющего его, устраняя их, когда это делает Java-код - независимо от того, что не нарушает семантику Java.
Вставка изменчивого чтения между этими двумя:
m_volatile = normal;
tmp = m_volatile; // "poison read"
m_normal = null;
неверно по другой причине, чем то, что Алексей Шипилев заявил в своем ответе: JMM имеет нулевые заявления об изменении порядка операций; устранение ненаблюдаемого "ядовитого чтения" никогда не изменяет порядок (никогда не устраняет барьеров) любых операций. Фактическая проблема с "poison read"
находится в get()
.
Предположим, что m_normal
читает в get()
наблюдает null
. Какая m_volatile
запись m_volatile
читать в get()
не разрешена не synchronize-with
? Проблема здесь в том, что разрешено появляться в общем порядке действий синхронизации до m_volatile
писать в change()
(получает упорядоченное с m_normal
чтение в get()
), поэтому наблюдайте начальную null
в m_volatile
, а не synchronize-with
записать в m_volatile
в change()
. Вам понадобится "полный барьер" перед тем, как m_volatile
читать в get()
- энергозависимый магазин. Который вы не хотите.
Кроме того, использование VarHandle.fullFence()
только в change()
не решит проблему по той же причине: гонка в get()
не устранена этим.
PS. Объяснение, приведенное Алексеем на https://shipilev.net/blog/2016/close-encounters-of-jmm-kind/#wishful-unobserved-volatiles, не верно. Там нет никаких исчезающих барьеров, допускаются только частичные заказы, где доступ к GREAT_BARRIER_REEF
отображается как первый и последний действия синхронизации соответственно.
Вы должны начать с предположения, что get()
разрешено возвращать null
. Тогда конструктивно докажите, что это недопустимо. Пока у вас не будет такого доказательства, вы должны предположить, что это может произойти.
Пример, где вы можете доказать конструктивно, что null
не разрешено:
volatile boolean m_v;
volatile Object m_volatile;
Object m_normal = new Object();
public void change() {
Object normal;
normal = m_normal;
if (normal == null) {
return;
}
m_volatile = normal; // W2
boolean v = m_v; // R2
m_normal = null;
}
public Object get() {
Object normal;
normal = m_normal;
if (normal != null) {
return normal;
}
m_v = true; // W1
return m_volatile; // R1
}
Теперь начните с предположения, что get()
может вернуться null
. Чтобы это произошло, get()
должен наблюдать null
в m_normal
и m_volatile
. Он может наблюдать null
в m_volatile
только тогда, когда R1
появляется перед W2
в общем порядке действий синхронизации. Но это означает, что R2
обязательно после W1
в этом порядке, поэтому synchronizes-with
it. Это устанавливает happens-before
между m_normal
, считанным в get()
и m_normal
, записывается в change()
, поэтому чтение m_normal
не позволяет наблюдать, что запись null
(не может наблюдать записи, которые происходят после читать) - противоречие. Поэтому исходное предположение о том, что как m_normal
, так и m_volatile
читает наблюдение null
, неверно: по крайней мере одно из них будет наблюдать непустое значение, и метод вернет это.
Если у вас нет W1
в get()
, в change()
нет ничего, что могло бы заставить ребро happens-before
между m_normal
read и m_normal
write - поэтому, заметив, что запись в get()
не противоречит JMM.