VarHandle обобщает доступ к volatile, разделяя доступ к памяти от семантики порядка памяти, применяемой к нему. В то время как volatile переменная всегда обеспечивает полное упорядочение (последовательную согласованность) для каждой операции чтения и записи, VarHandle предлагает четыре различных режима — plain, opaque, acquire/release и volatile — позволяя разработчикам выбирать более слабые модели согласованности, когда полная последовательная согласованность не является необходимой. Это разъединение позволяет продвинутым конкурентным алгоритмам избежать дорогих барьеров StoreLoad на архитектурах, таких как x86 или ARM, существенно увеличивая пропускную способность в таких сценариях, как очереди с одним производителем и одним потребителем. API достигает этого без обращения к sun.misc.Unsafe, предоставляя полностью поддерживаемый стандартный механизм для доступа вне кучи, манипуляции элементами массива и обновления полей записей с точными и поддающимися верификации семантиками памяти.
Мы оптимизировали кольцевой буфер без блокировок, используемый для телеметрии, где поток производителя записывал события, а поток потребителя обрабатывал их, оба работая с разделяемым массивом. Первоначальная реализация использовала volatile массив для элементов буфера, обеспечивая видимость, но вызывая полное ограничение памяти при каждом обновлении слота, что стало узким местом на наших серверах на базе ARM.
Первая рассматриваемая альтернатива — сохранить volatile и добавить выравнивание кэш-линий, чтобы избежать ложного совместного использования. Это сохранило корректность и уменьшило трафик согласованности кэша, но все же накладывало полную стоимость барьера StoreLoad, присущего volatile, расходуя ценные циклы ЦП для гарантий порядка, которые нам не требовались между производителем и потребителем.
Мы оценили возможность вернуться к synchronized блокам, защищающим индексы буфера, что упростило бы рассуждения о безопасности, обеспечивая взаимное исключение. К сожалению, этот подход сериализовал операции производителя и потребителя, уничтожая свойства задержки без блокировок, важные для наших целей обработки менее чем за миллисекунду и вводя риски инверсии приоритетов под тяжелой нагрузкой.
Мы приняли VarHandle с setRelease для записей производителя и getAcquire для чтений потребителя. Эта комбинация обеспечила необходимую зависимость happens-before между записью и последующим чтением, не накладывая полное упорядочение по отношению к другим переменным, идеально соответствуя модели памяти, необходимой для нашей очереди с одним производителем и одним потребителем.
В результате пропускная способность увеличилась примерно на сорок процентов на серверах ARM по сравнению с базовой линией volatile, сохраняя корректность, что демонстрирует, что более слабые модели согласованности достаточны, когда алгоритмическое проектирование уже ограничивает паттерныConcurrency.
Является ли VarHandle лишь безопасной оболочкой вокруг Unsafe для доступа к памяти вне кучи?
Хотя VarHandle может управлять сегментами вне кучи через MemorySegment, его основное архитектурное достижение заключается в том, чтобы предоставить режимы порядка памяти, которые Unsafe только приближенно реализовал с помощью непрозрачных барьеров. VarHandle позволяет объявлять, участвует ли доступ в порядке синхронизации (acquire/release) или просто гарантирует атомарность (opaque), различия, которые сырой putOrdered из Unsafe смешивал или требовал ручной вставки барьера для корректного приближения, что делает верификацию кода по отношению к JMM значительно более надежной.
Гарантирует ли setOpaque, что моя запись в конечном итоге станет видимой для другого потока?
Нет. Режим Opaque обеспечивает атомарность и согласованность — запись кажется неделимой и упорядоченной относительно других непрозрачных доступов к той же переменной, — но не предоставляет никаких гарантий happens-before между потоками. Поток, читающий с помощью getOpaque, может зациклиться, вечно наблюдая устаревшее кэшированное значение, если только другой механизм синхронизации не принудит сброс кэша, в отличие от acquire/release, который создает необходимую видимость между писателем и читателем.
Когда мне следует предпочесть режим volatile над setRelease/getAcquire?
Предпочитайте volatile, когда вам требуется последовательная согласованность: полное упорядочение всех операций volatile относительно друг друга в глобальном порядке синхронизации. Используйте acquire/release, когда вам необходимо обеспечить упорядочение между определенной записью и последующим чтением (безопасность публикации) без координации со всеми другими доступами к памяти. Неправильное применение acquire/release к алгоритмам, предполагающим последовательную согласованность, приводит к тонким ошибкам переупорядочивания, когда независимые обновления переменных кажутся выходящими из порядка для различных наблюдателей.