Поведение барьера памяти в Java
После прочтения большего количества блогов / статей и т. Д., Я действительно запутался в поведении загрузки / хранения до / после барьера памяти.
Ниже приведены две цитаты Дуга Ли в одной из его разъясняющих статей о JMM, которые очень просты:
- Все, что было видно потоку A, когда он записывает в переменное поле f, становится видимым потоку B, когда он читает f.
- Обратите внимание, что для обоих потоков важно получить доступ к одной и той же энергозависимой переменной, чтобы правильно установить отношение "до". Это не тот случай, когда все видимое для потока A, когда оно записывает изменяемое поле f, становится видимым для потока B после того, как оно читает изменяемое поле g.
Но потом, когда я заглянул в другой блог о барьере памяти, я получил такие:
- Барьер хранилища, инструкция "sfence" на x86, заставляет все инструкции хранилища до появления барьера раньше барьера и сбрасывает буферы хранилища в кэш для процессора, на котором он был выпущен.
- Барьер загрузки, инструкция "lfence" на x86, заставляет все инструкции загрузки после барьера следовать за барьером, а затем ждать загрузки буфера загрузки для этого ЦП.
На мой взгляд, разъяснение Дуга Ли более строгое, чем у другого: в основном это означает, что если барьер нагрузки и барьер хранилища находятся на разных мониторах, согласованность данных не гарантируется. Но последнее означает, что даже если барьеры находятся на разных мониторах, согласованность данных будет гарантирована. Я не уверен, правильно ли я понимаю эти 2, а также я не уверен, какой из них является правильным.
Учитывая следующие коды:
public class MemoryBarrier {
volatile int i = 1, j = 2;
int x;
public void write() {
x = 14; //W01
i = 3; //W02
}
public void read1() {
if (i == 3) { //R11
if (x == 14) //R12
System.out.println("Foo");
else
System.out.println("Bar");
}
}
public void read2() {
if (j == 2) { //R21
if (x == 14) //R22
System.out.println("Foo");
else
System.out.println("Bar");
}
}
}
Допустим, у нас есть 1 поток записи TW1, сначала вызывающий метод write() модуля MemoryBarrier, затем у нас есть два потока чтения TR1 и TR2, вызывающие методы read1() и read2() MemoryBarrier. Рассмотрим эту программу, работающую на ЦП, которая не сохраняет порядок (x86 Сохраняйте порядок для таких случаев, что не так), в соответствии с моделью памяти, будет барьер StoreStore (скажем, SB1) между W01/W02, а также барьер 2 LoadLoad между R11/R12 и R21/R22 (давайте скажем RB1 и RB2).
- Поскольку SB1 и RB1 находятся на одном мониторе i, поэтому поток TR1, который вызывает read1, всегда должен видеть 14 на x, также всегда печатается "Foo".
- SB1 и RB2 находятся на разных мониторах, если Даг Ли корректен, поток TR2 не будет гарантированно видеть 14 на x, что означает, что "Штрих" может иногда печататься. Но если барьер памяти работает, как Мартин Томпсон, описанный в блоге, барьер Store вытолкнет все данные в основную память, а барьер загрузки перетянет все данные из основной памяти в кэш / буфер, тогда TR2 также будет гарантированно видеть 14 на x.
Я не уверен, какой из них правильный, или оба они, но то, что описал Мартин Томпсон, относится только к архитектуре x86. JMM не гарантирует, что изменение x видимо для TR2, но реализация x86 делает.
Благодаря ~
2 ответа
Даг Ли прав. Вы можете найти соответствующую часть в разделе §17.4.4 Спецификации языка Java:
[..] Запись в энергозависимую переменную v (§8.3.1.4) синхронизируется со всеми последующими чтениями v любым потоком (где "последующий" определяется в соответствии с порядком синхронизации). [..]
Модель памяти конкретной машины не имеет значения, потому что семантика языка программирования Java определяется в терминах абстрактной машины - независимо от конкретной машины. Обязанностью среды выполнения Java является выполнение кода таким образом, чтобы он соответствовал гарантиям, данным Спецификацией языка Java.
По фактическому вопросу:
- Если нет дальнейшей синхронизации, метод
read2
можно распечатать"Bar"
, так какread2
может быть выполнен раньшеwrite
, - Если есть дополнительная синхронизация с
CountDownLatch
чтобы убедиться, чтоread2
выполняется послеwrite
тогда методread2
никогда не будет печатать"Bar"
потому что синхронизация сCountDownLatch
удаляет гонку данных наx
,
Независимые переменные переменные:
Имеет ли смысл, что запись в энергозависимую переменную не синхронизируется с чтением любой другой энергозависимой переменной?
Да, это имеет смысл. Если два потока должны взаимодействовать друг с другом, они обычно должны использовать один и тот же volatile
переменная для обмена информацией. С другой стороны, если поток использует изменчивую переменную без необходимости взаимодействия со всеми другими потоками, мы не хотим платить за барьер памяти.
Это на самом деле важно на практике. Давайте сделаем пример. Следующий класс использует переменную типа volatile:
class Int {
public volatile int value;
public Int(int value) { this.value = value; }
}
Представьте, что этот класс используется только локально в методе. JIT-компилятор может легко обнаружить, что объект используется только в этом методе ( Escape analysis).
public int deepThought() {
return new Int(42).value;
}
С вышеупомянутым правилом JIT-компилятор может удалить все эффекты volatile
читает и пишет, потому что volatile
переменная не может быть доступа из любого другого потока.
Эта оптимизация фактически существует в компиляторе Java JIT:
Насколько я понял, вопрос на самом деле о нестабильном чтении / записи и о том, что происходит перед гарантиями. Говоря об этой части, у меня есть только одно, что можно добавить к ответу nosid:
Энергозависимые записи не могут быть перемещены до нормальных записей, энергозависимые чтения не могут быть перемещены после обычных чтений. Вот почему read1()
а также read2()
результаты будут, как писал nosid.
Говоря о барьерах - определение звучит хорошо для меня, но одна вещь, которая, вероятно, смутила вас, состоит в том, что это вещи / инструменты / способ / механизм (назовите это как хотите) для реализации поведения, описанного в JMM в hotspot. При использовании Java вы должны полагаться на гарантии JMM, а не на детали реализации.