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

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

fullFence, acquireFence, releaseFence, loadLoadFence и storeStoreFence.

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

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

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

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

    • #fullFence is compatible to 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 ++, например, изменения, внесенные до барьерной инструкции release, видны потоку, выполняющему барьерную инструкцию collect.

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

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

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

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


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


Ответы (1)


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

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

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

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

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

Другой пример - когда (в 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 имеет два основных способа доступа к полю: plain и volatile (что гарантирует последовательную согласованность). Все эти методы, которые вы упомянули, предназначены для того, чтобы внести что-то среднее между этими двумя: семантика выпуска / получения; я думаю, есть случаи, когда людям на самом деле это нужно.

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


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

person Eugene    schedule 08.02.2020
comment
Не пытайтесь понять значение lazySet, ссылаясь на древние ответы, текущая документация точно говорит, что это означает в настоящее время. Кроме того, неверно утверждать, что JMM имеет только два режима доступа. У нас есть volatile read и volatile write, которые вместе могут установить связь происходит до. - person Holger; 18.02.2020
comment
@Holger Я сомневаюсь, что многие люди действительно понимают, что должно означать это setRelease из документации, но я согласен, что если вы даже планируете использовать, документации будет достаточно. Я также согласен, что это пара (read/write из volatile), но между ними ничего нет. До этих методов у вас был либо простой, либо volatile доступ (последовательная согласованность), ничего между ними. По крайней мере, я думаю, ничего промежуточного с гарантиями. - person Eugene; 18.02.2020
comment
Я как раз писал об этом что-то еще. Учтите, что cas - это и чтение, и запись, действующие как полный барьер, и вы можете понять, почему расслабление желательно. Например. при реализации блокировки первое действие - это cas (0, 1) по счетчику блокировок, но вам нужно только получить семантику (например, непостоянное чтение), тогда как окончательная запись 0 для разблокировки должна иметь семантику выпуска (например, изменчивая запись) , так что между разблокировкой и последующей блокировкой существует "случилось-до". Acquire / Release даже слабее, чем Volatile Read / Write, в отношении потоков, использующих различные блокировки. - person Holger; 18.02.2020
comment
Общий комментарий: будьте осторожны при просмотре asm: трудно понять, какие барьерные эффекты гарантированы каким-либо стандартом, а какие являются деталями реализации конкретной JVM (или компилятора C ++ для ограничений на этом языке). Если оптимизация все еще происходит, вы можете быть уверены (исключая ошибку компилятора), что барьер не останавливает ее, но если он действительно блокирует переупорядочение / устранение мертвого хранилища или что-то еще, это не всегда доказывает что-либо о языковой стандарт. - person Peter Cordes; 18.02.2020
comment
@PeterCordes, верно, я редко смотрю на сборку это, чтобы заметить это; и когда я это делаю, я смотрю только на некоторые примеры, которые публикуют эксперты JVM, редко выходя на улицу. Для меня это слишком сложно (и отнимает много времени). Я обычно читаю ваши ответы по этому поводу, кстати. - person Eugene; 18.02.2020
comment
Я действительно не знаю Java, но мне немного интересно, как проектировать языки и как языки, отличные от C / C ++, раскрывают атомику. Похоже, Java Opaque похож на нестабильность C ++: оптимизатор не видит ничего, кроме этого, и должен загружать и сохранять, когда об этом говорит источник? Это было бы эквивалентно C ++ memory_order_relaxed, который похож на C ++ volatile на реальные машины (с согласованной общей памятью), за исключением того, что C ++ volatile не поддерживает атомарные операции RMW; v++ - это отдельная загрузка, магазин - person Peter Cordes; 18.02.2020
comment
@PeterCordes: да, непрозрачность похожа на переменную в C ++; сохранение и загрузка происходит точно так же, как в исходном коде, без каких-либо оптимизаций; Я также признаю, что уже довольно давно готовлю вопрос о том, что именно это означает ... спасибо за ваши комментарии. - person Eugene; 18.02.2020
comment
Подождите, просто как C ++ volatile, с официально неопределенным поведением, если вы пишете из одного потока и читаете из другого? Или Java гарантирует, что базовая машина имеет согласованную разделяемую память, поэтому требование об отсутствии оптимизации в дополнение к этому дает видимость между потоками? И Java также гарантирует атомарность даже для opaque int64 или чего-то еще? C ++ - нет. Также локально упорядочивается C ++ volatile. другие изменчивые обращения (но не простые переменные); порядок видимости для других потоков зависит от модели аппаратной памяти машины. Или это действительно больше похоже на atomic с mo_relaxed? - person Peter Cordes; 18.02.2020
comment
(Компиляторы C ++ могут переупорядочивать более релаксированные атомарные обращения друг к другу и на неатомарные обращения, так что это главное отличие от C ++ volatile. Стандарт не запрещает оптимизацию атомики (например, свертывание двух расслабленных хранилищ спиной к спине), но на практике никакие компиляторы этого не делают, потому что вы можете гуглить ...) - person Peter Cordes; 18.02.2020
comment
@PeterCordes AFAIK, это больше похоже на mo_relax и действительно гарантирует атомарность для long (у нас нет int64). Полагаю, это заставляет меня ошибаться в предыдущем заявлении о volatile, за что мне очень жаль. volatile в java не разрешается переупорядочивать с другими летучими компонентами (что нарушило бы последовательную согласованность?). Я мало знаю о C ++ volatile, но вы заставили меня захотеть узнать о нем больше. - person Eugene; 18.02.2020
comment
Изменчивый ISO C ++ означает, что не нужно оптимизировать, и это все. По-прежнему Undefined Behavior писать в одном потоке и читать в другом; он разработан для MMIO. (Вам может понадобиться volatile std::atomic<int> для регистра MMIO, к которому обращаются несколько потоков). Он не имеет никакой гарантии того, что его можно будет использовать в разных потоках. (Но на практике это так, в обычных реализациях для типов с шириной указателя и меньше, потому что обычные ABI требуют, чтобы они были естественным образом выровнены, а в asm это дает атомарность. И, конечно, нормальные реализации выполняются на оборудовании с когерентным кешем.) - person Peter Cordes; 18.02.2020
comment
Конечно, при этом до C ++ 11 в C ++ даже не было модели памяти, и выкатывание ваших собственных атомик из volatile (и встроенного asm для барьеров памяти во время компиляции / выполнения и операций RMW) было довольно намного единственный вариант. И в реальной жизни компиляторы действительно поддерживают C / C ++ volatile таким образом, чтобы это можно было использовать; ядро Linux по-прежнему делает это. - person Peter Cordes; 18.02.2020
comment
@Holger, правда в том, что мне трудно, когда именно мне нужно выпускать / приобретать по сравнению с последовательной согласованностью. Ваш пример имеет смысл, но означает ли это, что мы можем заменить каждый cas на выпуск / приобретение? это зависит от того, что нужно вызывающему абоненту. - person Eugene; 18.02.2020
comment
Я знаю, что Java volatile не может изменять порядок с помощью других изменчивых доступов, это похоже на C ++ atomic<> с порядком seq_cst по умолчанию. Мой вопрос заключался в том, может ли Java Opaque переупорядочиваться с помощью других непрозрачных доступов, таких как атомарный C ++ с mo_relaxed, или он упорядочен по отношению к. другие непрозрачные обращения, такие как C ++ volatile. (Я повторяю свою точку зрения о том, что volatile - ужасный выбор названия для атомики Java!) - person Peter Cordes; 18.02.2020
comment
re: acq / rel vs. seq_cst: для реализации в такой модели памяти, как x86 (seq_cst + буфер хранилища), acq / rel не нуждается в каких-либо барьерах. SC необходимо дождаться, пока буфер хранилища опустеет (полный барьер) после хранилища, чтобы заблокировать переупорядочивание StoreLoad preshing.com/20120515/memory-reordering-caught-in-the-act. Некоторым CAS требуется seq_cst, некоторым вариантам использования нужен только acq_rel. Блокировка - это вариант использования, который технически требует только acq / rel, поэтому более ранние и последующие операции могут переупорядочить в критическую секцию, но вещи из критической секции не могут выйти. preshing.com/20120913/acquire-and-release-semantics - person Peter Cordes; 18.02.2020
comment
@Peter Cordes: Первой версией C с ключевым словом volatile была C99, через пять лет после Java, но ей все еще не хватало полезной семантики, даже C ++ 03 не имеет модели памяти. То, что C ++ называет атомарным, также намного моложе Java. А ключевое слово volatile даже не подразумевает атомарных обновлений. Так с чего бы его так называть. - person Holger; 18.02.2020
comment
@Holger: О, я не знал, что volatile был такой же недавней, как C99, или что Java имела volatile для многопоточности еще в 1999 году. Ранее я прокомментировал, что C ++ даже не имеет (ориентированной на поток) модели памяти. пока C ++ 11 не представил это и std::atomic. (То же самое для C11 и stdatomic / _Atomic). Я думаю, что варианты именования C / C ++ имеют смысл: volatile = не оптимизировать, можно использовать для MMIO / взаимодействия с базовой машиной, когда вам важно, как что-то компилируется. atomic<> и atomic_flag: межпотоковое поведение, гарантированное языковым стандартом, независимо от деталей реализации / HW. - person Peter Cordes; 18.02.2020
comment
@Holger: Я говорил, что Java volatile кажется плохим выбором имени, учитывая то значение, которое оно имеет в Java. (И я подумал, потому что C уже использовал его для чего-то совсем другого.) Хм, javarevisited.blogspot.com/2011/06/ говорит, что Java 5 добавила семантику SC в Java volatile, так что, может быть, раньше это было больше похоже на C? И вместо того, чтобы ввести новое имя, как это сделал C ++, они просто изменили поведение. - person Peter Cordes; 18.02.2020
comment
Хм, en.wikipedia.org/wiki/Volatile_(computer_programming)#In_Java говорит, что в Java всегда был некоторый порядок порядка volatile. - person Peter Cordes; 18.02.2020
comment
@Holger: en.cppreference.com/w/c/language/volatile говорит, что новая вещь в C99 - возможность использовать синтаксис void f(double x[volatile]) вместо void f(double *x). Помимо этого, C volatile существует с C89 и, вероятно, несколько раньше, во времена K&R. Я почти уверен, что вы ошибаетесь, что volatile были новичком в C99 и, следовательно, пост-датировкой Java. (Но да, Java volatile восходит к оригинальной Java в 1995 году, еще до того, как многопоточное программирование было таким большим делом. Имеет смысл, что они просто скопировали квалификатор из C / C ++ (у которого не было модели памяти в время)) - person Peter Cordes; 18.02.2020
comment
@PeterCordes 1) re opaque: документация setOpaque говорит Sets the value of a variable to the newValue, in program order..., для меня это означает отсутствие переупорядочения между самими непрозрачными. 2) на x86 все как-то хорошо понимают rel / acq и seq_const (и тот факт, что rel / acq в основном бесплатен), это другие платформы заботятся об этом гораздо больше. Это еще одна причина, по которой они ввели эти методы. - person Eugene; 18.02.2020
comment
@PeterCordes, возможно, я путаю это с restrict, однако я помню времена, когда мне приходилось писать __volatile, чтобы использовать расширение компилятора без ключевых слов. Так, может быть, он не полностью реализовал C89? Не говори мне, что я тот старый. До Java 5 volatile был намного ближе к C. Но в Java не было MMIO, поэтому ее целью всегда была многопоточность, но семантика до Java 5 была не очень полезна для этого. Таким образом, была добавлена ​​семантика выпуска / получения, но все же она не атомарна (атомарные обновления - это дополнительная функция, построенная поверх нее). - person Holger; 18.02.2020
comment
@Eugene относительно этого , мой пример был специфичен для использования cas для блокировки, которая будет приобретена. Защелка обратного отсчета будет иметь атомарные декременты с семантикой выпуска, за которой следует нить, достигающая нуля, вставляя ограждение получения и выполняя финальное действие. Конечно, есть и другие случаи, когда атомарные обновления требуют полной защиты. - person Holger; 18.02.2020
comment
@Holger понял, в таком случае это имеет смысл. как обычно от тебя. очень признателен. - person Eugene; 18.02.2020