Что действительно делает барьер LoadLoad?

В Java, когда у нас есть два потока, которые используют следующие переменные:

int a;
volatile int b;

если поток 1:

a = 5;
b = 6;

Затем между этими двумя инструкциями вставлен барьер StoreStore, а "a" сбрасывается обратно в основную память.

Теперь, если поток 2:

if(b == 6)
 a++;

барьер LoadLoad вставлен между ними, и мы гарантируем, что если новое значение "b" будет видимым, тогда также будет видно новое значение "a". Но как на самом деле это достигается? Загружает ли LoadLoad кэширование/регистры процессора? Или просто инструктирует CPU для получения значений переменных, которые следуют за чтением из volatile снова из CPU?

Я нашел эту информацию о барьерах LoadLoad (http://gee.cs.oswego.edu/dl/jmm/cookbook.html):

Барьеры LoadLoad Последовательность: Load1; LoadLoad; Load2 гарантирует, что Данные Load1 загружаются до доступа к данным Load2 и всех загружаются последующие инструкции по загрузке. В общем, явный LoadLoad необходимы барьеры для процессоров, выполняющих спекулятивные нагрузки и/или обработки вне очереди, в которой инструкции ожидающей нагрузки могут байпасные магазины ожидания. О процессорах, гарантирующих всегда сохранение порядок загрузки, барьеры равны нулю.

но на самом деле это не объясняет, как это достигается.

Ответ 1

Я приведу один пример о том, как это достигается. Вы можете подробнее прочитать подробности здесь. Для x86-процессоров, как вы указали, LoadLoad заканчивается без операций. В статье, которую я связал, Марк указывает, что

Doug перечисляет StoreStore, LoadLoad и LoadStore

Таким образом, по существу единственным необходимым барьером является архитектура StoreLoad для x86. Итак, как это достигается на низком уровне?

Это выдержка из блога:

Здесь код, который он сгенерировал как для энергозависимых, так и для нестабильных значений:

nop                       ;*synchronization entry
mov    0x10(%rsi),%rax    ;*getfield x

И для летучих записей:

xchg   %ax,%ax
movq   $0xab,0x10(%rbx)
lock addl $0x0,(%rsp)     ;*putfield x

Инструкция lock - это StoreLoad, как указано в кулинарной книге Дуга. Но инструкция блокировки также синхронизирует все чтения с другими процессами как в списке

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

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

Все сказанное, я повторю то, что заметили ассирийцы. То, как это происходит, не должно быть важно для разработчика (если вы заинтересованы в реализации процессора/компилятора, это еще одна история). Ключевое слово volatile - это вид интерфейса, говорящего

  • Вы получите самое свежее чтение, которое написано другим потоком.
  • Вы не сможете сжечь оптимизацию компилятора JIT.

Ответ 2

Если этот LoadLoad вычисляет значение no-op, то поток 2 может продолжать использовать кешированные значения.

Это покрывается таблицей "Can Order" в кулинарной книге.

Порядок программирования

read b
read a
write a

путем "кеширования a", вы имеете в виду, что код переупорядочен

read a
...
read b

Это переупорядочение запрещено.