Для чого використовуються огорожі пам'яті на 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 для нього ). Однак мені незрозуміло для чого це.


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

Відповіді:


11

Це, головним чином, невідповідь (спочатку хотілося зробити коментар, але, як бачите, це занадто довго). Просто я багато цього допитував, багато читав і досліджував, і в цей момент я сміливо можу сказати: це складно. Я навіть писав кілька тестів з jcstress, щоб зрозуміти, як насправді вони працюють (дивлячись на сформований код складання), і хоча деякі з них якимось чином мали сенс, тема взагалі аж ніяк не проста.

Перше, що вам потрібно зрозуміти:

Специфікація мови Java (JLS) ніде не згадує бар'єрів . Це, для Java, було б деталізацією реалізації: воно дійсно діє в сенсі, що трапляється до семантики. Щоб мати можливість правильно вказати їх відповідно до JMM (Java Memory Model), 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 має в основному два способи доступу до поля: звичайний і мінливий (що гарантує послідовну послідовність ). Усі ці методи, які ви згадуєте, є для того, щоб вивести щось середнє між цими двома - випустити / придбати семантику ; Я думаю, є випадки, коли люди насправді цього потребують.

Ще більше розслаблення від звільнення / придбання було б непрозорим , що я все ще намагаюся повністю зрозуміти .


Таким чином, підсумок (ваше розуміння досить правильне, btw): якщо ви плануєте використовувати це в java - у них наразі немає специфікації, робіть це на свій страх і ризик. Якщо ви хочете зрозуміти їх, їх C ++ еквівалентні режими - це місце для початку.


1
Не намагайтеся з’ясувати сенс lazySet, посилаючись на стародавні відповіді, в поточній документації точно сказано, що це означає сьогодні. Крім того, оманливо сказати, що у JMM є лише два режими доступу. У нас є нестабільне читання та непостійне записування , які разом можуть встановити відносини, що трапляються раніше .
Холгер

1
Я був в середині, щоб написати щось більше про це. Вважайте, що cas - це і читання, і запис, що діє як повний бар'єр, і ви можете зрозуміти, чому розслабляти його бажано. Наприклад, коли реалізується блокування, першою дією є cas (0, 1) на кількість заблокованих, але вам потрібно лише придбати семантичний (наприклад, непостійний зчитування), тоді як остаточне записування 0 для розблокування повинно мати семантичний випуск (наприклад, непостійне записування ), тому між розблокуванням та наступним блокуванням трапляється раніше . Придбати / випустити навіть слабкіше, ніж непостійні читання / записи щодо тем, що використовують різні блоки.
Холгер

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
@Eugene щодо цього , мій приклад був специфічним для використання cas для блокування, яке було б придбати. Засувка зворотного відліку мала б атомні декременти з семантичним випуском, за яким нитка доходила до нуля, вставляючи огорожу і виконуючи остаточну дію. Звичайно, є й інші випадки оновлення атомів, коли необхідний повний паркан.
Холгер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.