Для чего используются заборы памяти в Java?

18

Пытаясь понять, как SubmissionPublisher( исходный код в Java SE 10, OpenJDK | docs ), новый класс, добавленный в Java SE в версии 9, был реализован, я наткнулся на несколько вызовов API, о которых VarHandleраньше не знал:

fullFence, acquireFence, releaseFence, loadLoadFenceИ storeStoreFence.

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

  1. Барьеры памяти переупорядочивают ограничения относительно операций чтения и записи.

  2. Барьеры памяти можно разделить на две основные категории: однонаправленные и двунаправленные барьеры памяти, в зависимости от того, устанавливают ли они ограничения на чтение или запись, или на то и другое.

  3. C ++ поддерживает множество барьеров памяти , однако они не совпадают с теми, которые предоставляет VarHandle. Однако некоторые из имеющихся в наличии барьеров памяти VarHandleобеспечивают эффекты упорядочения , совместимые с соответствующими им барьерами памяти C ++.

    • #fullFence совместим с atomic_thread_fence(memory_order_seq_cst)
    • #acquireFence совместим с atomic_thread_fence(memory_order_acquire)
    • #releaseFence совместим с atomic_thread_fence(memory_order_release)
    • #loadLoadFenceи не #storeStoreFenceимеют совместимого контраргумента C ++

Слово « совместимость» кажется здесь действительно важным, поскольку семантика явно отличается, когда дело доходит до деталей. Например, все барьеры C ++ являются двунаправленными, в то время как барьеры Java не являются (обязательно).

  1. Большинство барьеров памяти также имеют эффекты синхронизации. Это особенно зависит от используемого типа барьера и ранее выполненных инструкций барьера в других потоках. Поскольку полное значение, которое имеет инструкция для барьера, зависит от аппаратного обеспечения, я буду придерживаться барьеров более высокого уровня (C ++). Например, в C ++ изменения, сделанные до инструкции барьера освобождения , видны потоку, выполняющему инструкцию барьера получения .

Верны ли мои предположения? Если да, то мои следующие вопросы:

  1. Существуют ли барьеры памяти в VarHandleкакой-либо синхронизации памяти?

  2. Независимо от того, вызывают ли они синхронизацию памяти или нет, для чего могут быть полезны ограничения переупорядочения в Java? Модель памяти Java уже дает некоторые очень строгие гарантии в отношении упорядочения, когда задействованы изменчивые поля, блокировки или VarHandleподобные операции #compareAndSet.

В случае, если вы ищете пример: вышеупомянутый BufferedSubscription, внутренний класс SubmissionPublisher(источник связан с выше), установил полный забор в строке 1079 (функция growAndAdd; поскольку связанный веб-сайт не поддерживает идентификаторы фрагмента, просто CTRL + F для него ). Однако для меня неясно, для чего это.

Quaffel
источник
1
Я пытался ответить, но, проще говоря, они существуют, потому что люди хотят более слабый режим, чем у Java. В порядке возрастания, это было бы: plain -> opaque -> release/acquire -> volatile (sequential consistency).
Евгений

Ответы:

11

На самом деле это в основном не ответ (изначально хотелось сделать это комментарием, но, как вы видите, это слишком долго). Просто я сам много сомневался в этом, много читал и исследовал, и на данный момент могу с уверенностью сказать: это сложно. Я даже написал несколько тестов для jcstress, чтобы выяснить, как они работают (глядя на сгенерированный код сборки) и хотя некоторые из них так или иначе смысл, тема в общем-то далеко не простая.

Самое первое, что вам нужно понять:

Спецификация языка Java (JLS) нигде не упоминает барьеры . Для Java это будет деталь реализации: она действительно действует в терминах , предшествующих семантике. Чтобы иметь возможность правильно указать их в соответствии с JMM (модель памяти Java), JMM пришлось бы изменить довольно сильно .

Эта работа в процессе.

Во-вторых, если вы действительно хотите поцарапать поверхность здесь, это самое первое, что нужно посмотреть . Разговор невероятный. Моя любимая часть - когда Херб Саттер поднимает свои 5 пальцев и говорит: «Вот так много людей могут действительно и правильно работать с ними». Это должно дать вам подсказку о сложности. Тем не менее, есть некоторые тривиальные примеры, которые легко понять (например, счетчик, обновленный несколькими потоками, который не заботится о других гарантиях памяти, а заботится только о том, чтобы он сам увеличивался правильно).

Другой пример - когда (в Java) вы хотите, чтобы volatileфлаг управлял потоками для остановки / запуска. Вы знаете, классический

volatile boolean stop = false; // on thread writes, one thread reads this    

Если вы работаете с Java, вы бы знали, что без volatile этого код нарушается (например, вы можете прочитать, почему блокировка двойной проверки нарушается без него). Но знаете ли вы, что для некоторых людей, которые пишут высокопроизводительный код, это слишком много? volatileчтение / запись также гарантирует последовательную согласованность - у этого есть некоторые сильные гарантии, и некоторые люди хотят более слабую версию этого.

Потокобезопасный флаг, но не изменчивый? Да, именно так : VarHandle::set/getOpaque.

И вы спросите, зачем кому-то это может понадобиться, например? Не все заинтересованы во всех изменениях, которые поддерживаются volatile.

Посмотрим, как мы этого добьемся в Java. Прежде всего, такие экзотические вещи уже существовали в API AtomicInteger::lazySet. Это не определено в модели памяти Java и не имеет четкого определения ; все еще люди использовали это (LMAX, afaik или это для большего чтения ). ИМХО, AtomicInteger::lazySetесть VarHandle::releaseFence(или VarHandle::storeStoreFence).


Давайте попробуем ответить, зачем это кому-то нужно ?

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

Еще большее расслабление от выпуска / приобретения было бы непрозрачным , что я все еще пытаюсь полностью понять .


Таким образом, суть (ваше понимание довольно правильное, кстати): если вы планируете использовать это в Java - у них нет спецификаций на данный момент, делайте это на свой страх и риск. Если вы действительно хотите понять их, то их стартовые режимы в C ++ - это то, что вам нужно.

Евгений
источник
1
Не пытайтесь выяснить значение lazySet, ссылаясь на древние ответы, текущая документация точно говорит, что это означает, в настоящее время. Кроме того, вводить в заблуждение тот факт, что JMM имеет только два режима доступа. У нас есть изменчивое чтение и изменчивое запись , которые вместе могут установить отношения « до того как случится» .
Хольгер
1
Я был в процессе написания чего-то большего об этом. Предположим, что cas - это и чтение, и запись, действующие как полный барьер, и вы можете понять, почему расслабление желательно. Например, при реализации блокировки первое действие - это cas (0, 1) для счетчика блокировок, но вам нужно только получить семантическую (например, volatile-чтение), тогда как окончательная запись 0 для разблокировки должна иметь семантическую версию (например, volatile-запись). ), так что между разблокировкой и последующей блокировкой происходит случайный случай. Acquire / Release даже слабее, чем Volatile Read / Write в отношении потоков, использующих различные блокировки.
Хольгер
1
@Peter Cordes: Первая версия C с volatileключевым словом была C99, пять лет спустя после Java, но ей все еще не хватало полезной семантики, даже в C ++ 03 нет модели памяти. Вещи, которые C ++ называет «атомарными», также намного моложе, чем Java. И volatileключевое слово даже не подразумевает атомных обновлений. Так почему это должно быть названо так.
Хольгер
1
Возможно, @PeterCordes, я путаю его с тем restrict, однако, я помню времена, когда мне приходилось писать, __volatileчтобы использовать расширение компилятора без ключевых слов. Так что, возможно, он не реализовал C89 полностью? Не говори мне, что я такой старый. До Java 5 он volatileбыл намного ближе к C. Но в Java не было MMIO, поэтому его целью всегда была многопоточность, но семантика до Java 5 была не очень полезна для этого. Таким образом, была добавлена ​​семантика релиза / приобретения, но, тем не менее, она не является атомарной (атомарные обновления - это дополнительная функция, встроенная в нее).
Хольгер
2
@ Евгений относительно этого , мой пример был специфичен для использования cas для блокировки, которую можно было бы получить. Защелка обратного отсчета будет содержать атомарные декременты с семантикой релиза, после чего поток достигнет нуля, вставляя ограничитель захвата и выполняя последнее действие. Конечно, есть и другие случаи для атомарных обновлений, когда требуется полная защита.
Хольгер