Что означает "последующее чтение" в контексте изменчивых переменных?

Документация по видимости памяти Java гласит, что:

Происходит запись в нестабильное поле - перед каждым последующим чтением этого же поля.

Я смущен тем, что делает последующие средства в контексте многопоточности. Это предложение подразумевает некоторые глобальные часы для всех процессоров и ядер. Так, например, я присваиваю значение переменной в цикле c1 в некотором потоке, а затем второй поток может видеть это значение в следующем цикле c1 + 1?

Ответ 1

Мне звучит так, как будто это говорит о том, что он обеспечивает блокировку получения/освобождения памяти, упорядочивая семантику между потоками. См. Статью Джеффа Прешинга, объясняющую концепцию (в основном для C++, но ее основная точка статьи является нейтральной для языка и о концепции.)

На самом деле Java volatile обеспечивает последовательную согласованность, а не только acq/rel. Однако фактической блокировки нет. См. Статью Jeff Preshing для объяснения того, почему именование соответствует тому, что вы сделали бы с блокировкой.)


Если читатель видит значение, которое вы написали, он знает, что все в потоке производителя до этой записи также уже произошло.

Эта гарантия заказа может быть полезна только в сочетании с другими гарантиями о заказе в пределах одной нити.

например

int data[100];
volatile bool data_ready = false;

Режиссер:

data[0..99] = stuff;
 // release store keeps previous ops above this line
data_ready = true;

Потребитель:

while(!data_ready){}     // spin until we see the write
// acquire-load keeps later ops below this line
int tmp = data[99];      // gets the value from the producer

Если data_ready не был волатильным, чтение его не установило бы связь между двумя потоками.

Вам не нужно иметь спинлооп, вы можете читать порядковый номер или индекс массива из volatile int, а затем читать data[i].


Я не очень хорошо знаю Java. Я думаю, что volatile фактически дает вам последовательную согласованность, а не только выпуск/приобретение. Хранилище с последовательным выпуском не разрешается переупорядочивать с более поздними нагрузками, поэтому на типичном аппаратном обеспечении ему необходим дорогой барьер для обеспечения безопасности, чтобы убедиться, что локальный буфер хранения основного ядра сброшен до того, как разрешены любые более поздние загрузки.

Volatile Vs Atomic объясняет больше о заказе volatile дает вам.

Java volatile - это просто ключевое слово для заказа; это не эквивалентно C11 _Atomic или C++ 11 std::atomic<T> которые также дают вам атомные операции RMW. В Java volatile_var++ не является атомарным приращением, он представляет собой отдельный volatile_var++ и хранилище, например volatile_var = volatile_var + 1. В Java вам нужен класс AtomicInteger для получения атомного RMW.

И обратите внимание, что C/C++ volatile не подразумевает атомарность или упорядочение вообще; он только сообщает компилятору предположить, что значение может быть изменено асинхронно. Это лишь небольшая часть того, что вам нужно писать беззаботным для чего угодно, кроме простейших случаев.

Ответ 2

Я смущен тем, что делает последующие средства в контексте многопоточности. Это предложение подразумевает некоторые глобальные часы для всех процессоров и ядер...?

Последующие средства (согласно словарю), поступающие по времени. Конечно, есть глобальные часы для всех процессоров на компьютере (думаю, X Ghz), и документ пытается сказать, что если thread-1 сделал что-то в такте tick 1, то thread-2 что-то делает на другом CPU с тактовой частотой 2, это действия считаются последующими.

Происходит запись в нестабильное поле - перед каждым последующим чтением этого же поля.

Ключевая фраза, которую можно добавить к этому предложению, чтобы сделать ее более понятной, - "в другом потоке". Это может иметь смысл понять это как:

Происходит запись в поле volatile - перед каждым последующим чтением этого же поля в другом потоке.

Это говорит о том, что если чтение volatile поля происходит в Thread-2 после (по времени) записи в Thread-1, то Thread-2 будет гарантированно видеть обновленное значение. Далее в документации, на которую вы указываете, находится раздел (основное внимание):

... Результаты записи одним потоком гарантированно будут видны для чтения другим потоком только в том случае, если операция записи происходит до операции чтения. Синхронизированные и изменчивые конструкции, а также методы Thread.start() и Thread.join() могут формироваться в отношениях-до отношений. Особенно.

Обратите внимание на выделенную фразу. Компилятор Java может свободно переупорядочивать инструкции в любом поточном исполнении для целей оптимизации, если переупорядочение не нарушает определение языка - это называется порядком выполнения и критически отличается от порядка программирования.

Рассмотрим следующий пример: переменные a и b которые являются энергонезависимыми, инициализируются до 0 без synchronized предложений. Показан порядок программы и время, в которое потоки сталкиваются с строками кода.

Time     Thread-1        Thread-2
1        a = 1;          
2        b = 2;          
3                        x = a;
4                        y = b;
5        c = a + b;      z = x + y;

Если Thread-1 добавляет a + b во время 5, то гарантировано будет 3. Однако, если Thread-2 добавляет x + y в момент 5, он может получить 0, 1, 2 или 3 в зависимости от условий гонки. Зачем? Поскольку компилятор мог переупорядочить инструкции в Thread-1, чтобы установить a после b из соображений эффективности. Кроме того, Thread-1 может не правильно опубликовать значения a и b чтобы Thread-2 мог получить устаревшие значения. Даже если Thread-1 получает контекстную коммутацию или пересекает барьер памяти записи, а a и b публикуются, Thread-2 должен пересечь барьер чтения для обновления любых кешированных значений a и b.

Если и a b были отмечены как volatile то запись в должно произойти, прежде (с точкой зрения видимости гарантий) последующее считывание из в строке 3, а запись в a a b должна произойти, перед последующим считыванием b на линии 4. Оба потока получат 3.

Мы используем volatile и synchronized ключевые слова в java для обеспечения того, что происходит до гарантий. Предел памяти записи пересекается при назначении volatile или выходящего из synchronized блока, а барьер чтения пересекается при чтении volatile или ввода synchronized блока. Компилятор Java не может изменить порядок написания инструкций после этих барьеров памяти, чтобы обеспечить порядок обновлений. Эти ключевые слова управляют переупорядочением инструкций и гарантируют правильную синхронизацию памяти.

ПРИМЕЧАНИЕ. volatile не требуется в однопоточном приложении, потому что порядок программы гарантирует, что чтение и запись будут согласованы. Однопоточное приложение может видеть любое значение (энергонезависимое) a и b в моменты 3 и 4, но оно всегда видит 3 в момент 5 из-за языковых гарантий. Поэтому, несмотря на то, что использование volatile изменения изменяет поведение переупорядочения в однопоточном приложении, оно требуется только при совместном использовании данных между потоками.

Ответ 3

Это означает, что, как только определенный поток записывает в нестабильное поле, все остальные Thread (s) будут наблюдать (при следующем чтении) это письменное значение; но это не защищает вас от рас.

В потоках есть свои кеши, и эти кеши будут аннулированы и обновлены с помощью этого нового значения через протокол когерентности кеша.

РЕДАКТИРОВАТЬ

Последующее означает, когда это происходит после самой записи. Поскольку вы не знаете точный цикл/время, когда это произойдет, вы обычно говорите, когда какой-то другой поток наблюдает за записью, он будет наблюдать за всеми действиями, которые были сделаны до этого; таким образом, волатильность устанавливает гарантии перед гарантиями.

Пример как в примере:

 // Actions done in Thread A
 int a = 2;
 volatile int b = 3;


 // Actions done in Thread B
 if(b == 3) { // observer the volatile write
    // Thread B is guaranteed to see a = 2 here
 }

Вы также можете зацикливать (ожидание вращения) до тех пор, пока не увидите 3.

Ответ 4

Это скорее определение того, что не произойдет, а не того, что произойдет.

По сути, он говорит, что после того, как произошла запись в atomic переменную, не может быть никакого другого потока, который при чтении переменной прочитает устаревшее значение.

Рассмотрим следующую ситуацию.

  • Thread A непрерывно увеличивает atomic значение a.

  • Thread B иногда читает Aa и предоставляет это значение как неатомную переменную b.

  • Thread C иногда читает как Aa и Bb.

Учитывая, что a является atomic можно рассуждать о том, что с точки зрения С b может иногда быть меньше a но никогда не будет больше a.

Если бы a не было атомным, такая гарантия не могла быть дана. При определенных ситуациях кэширования было бы вполне возможно для C, чтобы увидеть b прогресс вне в любое время. a

Это упрощенная демонстрация того, как модель памяти Java позволяет вам рассуждать о том, что может и не может произойти в многопоточной среде. В реальной жизни потенциальные условия гонки между чтением и записью в структуры данных могут быть намного сложнее, но процесс рассуждений одинаков.

Ответ 5

Ответ Питера дает обоснование дизайна модели памяти Java.
В этом ответе я пытаюсь дать объяснение, используя только понятия, определенные в JLS.


В Java каждый поток состоит из набора действий.
Некоторые из этих действий могут быть наблюдаемы другими потоками (например, написанием общей переменной), они называются действиями синхронизации.

Порядок, в котором действия потока записываются в исходный код, называется порядком программы.
Порядок определяет, что есть прежде и что после (или лучше, а не раньше).

Внутри потока каждое действие имеет отношение "раньше" (обозначается символом <) со следующим действием (в порядке выполнения программы). Эта взаимосвязь важна, но трудно понять, потому что она очень фундаментальна: она гарантирует, что если A <B, то "эффекты" A видны B.
Это действительно то, что мы ожидаем при написании кода функции.

Рассматривать

Thread 1           Thread 2

  A0                 A'0
  A1                 A'1
  A2                 A'2
  A3                 A'3

Тогда по порядку программы мы знаем A0 <A1 <A2 <A3 и что A'0 <A'1 <A'2 <A'3.
Мы не знаем, как упорядочить все действия.
Он мог бы быть A0 <A'0 <A'1 <A'2 <A1 <A2 <A3 <A'3 или последовательность с заменой простых чисел.
Однако каждая такая последовательность должна иметь то, что отдельные действия каждого потока упорядочены в соответствии с порядком программы потока.

Двух программных заказов недостаточно, чтобы заказывать каждое действие, это частичные заказы, в противоположность общему порядку, который мы ищем.

Общий порядок, который помещает действия в строке в соответствии с измеряемым временем (например, тактами), что они произошли, называется порядком выполнения.
Это порядок действий на самом деле (требуется только, чтобы действия выполнялись в этом порядке, но это просто детали оптимизации).

До сих пор действия не упорядочены между потоками (между двумя разными потоками).
Для этой цели служат действия синхронизации.
Каждое действие синхронизации синхронизируется, по крайней мере, с другим действием синхронизации (обычно они попадают парами, такими как запись и чтение изменчивой переменной, блокировка и разблокировка мьютекса).

Синхронизация с отношением - это происхождение-прежде между потоком (первое из них подразумевает последнее), оно раскрывается как другое понятие, потому что 1) оно немного 2) происходит, - прежде чем принудительно выполняется аппаратное обеспечение, а синхронизация - может потребовать программного обеспечения.

произойдет-прежде, происходит из заказа программы, синхронизируется с порядком синхронизации (обозначается как <<).
Порядок синхронизации определяется в двух свойствах: 1) это полный порядок 2) он согласуется с порядком программы каждого потока.

Добавьте некоторые действия синхронизации в наши потоки:

Thread 1           Thread 2

  A0                 A'0
  S1                 A'1
  A1                 S'1
  A2                 S'2
  S2                 A'3

Заказы на программу тривиальны.
Что такое порядок синхронизации?

Мы ищем что-то, что по 1) включает все S1, S2, S'1 и S'2 и 2) должно иметь S1 <S2 и S'1 <S'2.

Возможные последствия:

S1 < S2 < S'1 < S'2
S1 < S'1 < S'2 < S2
S'1 < S1 < S'2 < S'2

Все являются порядками синхронизации, нет одного порядка синхронизации, но многие, вопрос выше неправильный, это должно быть "Что такое заказы синхронизации?".

Если S1 и S'1 таковы, что S1 << S'1, чем мы ограничиваем возможные результаты теми, где S1 <S'2, поэтому исход S'1 <S1 <S'2 <S'2 выше теперь запрещено.

Если S2 << S'1, то единственным возможным результатом является S1 <S2 <S'1 <S'2, когда есть только один результат, я считаю, что мы имеем последовательную согласованность (обратное неверно).

Заметим, что если A << B это не означает, что в коде есть механизм, чтобы заставить порядок выполнения, где A <B.
На действия синхронизации влияет порядок синхронизации, на которые они не налагают никакой материализации.
Некоторые действия синхронизации (например, блокировки) налагают конкретный порядок выполнения (и, следовательно, порядок синхронизации), а некоторые нет (например, чтение/запись летучих).
Это порядок выполнения, который создает порядок синхронизации, это полностью ортогонально отношению синхронизации.


Короче говоря, "последующее" прилагательное относится к любому порядку синхронизации, то есть к любому действительному (в соответствии с порядком последовательности нитей) порядку, который охватывает все действия синхронизации.


Затем JLS продолжает определять, когда происходит гонка данных (когда два конфликтующих доступа не упорядочены по-прежнему) и что это означает, что происходит - до согласования.
Они не входят в сферу охвата.