JEP 193: Variable Handles
Variable Handles: типизированные ссылки на переменные
| Автор | Doug Lea |
| Ответственный | Paul Sandoz |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 9 |
| Компонент | core-libs / java.lang |
| Обсуждение | core dash libs dash dev at openjdk dot java dot net |
| Трудоёмкость | M |
| Длительность | L |
| Связан с | JEP 266: More Concurrency Updates |
| Рецензенты | Dave Dice, Paul Sandoz |
| Одобрен | Brian Goetz |
| Создан | 2014/01/06 20:00 |
| Обновлён | 2017/08/17 16:45 |
| Задача | 8046183 |
Аннотация
Определить стандартный способ вызывать аналоги различных операций java.util.concurrent.atomic и sun.misc.Unsafe над полями объектов и элементами массивов, стандартный набор барьерных операций (fence) для тонкого управления упорядочиванием доступа к памяти и стандартную операцию reachability fence, гарантирующую, что объект, на который есть ссылка, остаётся сильно достижимым.
Цели
Обязательные цели:
-
Безопасность. Не должно быть возможности привести виртуальную машину Java в состояние с повреждённой памятью. Например, поле объекта можно обновить только экземплярами, которые приводятся к типу поля, а к элементу массива можно обратиться, только если индекс находится в границах массива.
-
Целостность. Доступ к полю объекта подчиняется тем же правилам доступа, что и байт-коды
getfieldиputfield, с дополнительным ограничением: поле объекта с модификаторомfinalнельзя обновить. (Примечание: те же правила безопасности и целостности применяются и кMethodHandles, дающим доступ к полю на чтение или запись.) -
Производительность. Характеристики производительности должны совпадать или быть близки к характеристикам эквивалентных операций
sun.misc.Unsafe(в частности, сгенерированный ассемблерный код должен быть почти идентичным с точностью до некоторых проверок безопасности, которые нельзя устранить свёрткой). -
Удобство использования. API должен быть лучше, чем API
sun.misc.Unsafe.
Желательно, но не обязательно, чтобы API был так же хорош, как API java.util.concurrent.atomic.
Мотивация
По мере того как конкурентное и параллельное программирование на Java продолжает развиваться, программистов всё сильнее раздражает невозможность использовать конструкции Java для атомарных или упорядоченных операций над полями отдельных классов, например для атомарного инкремента поля count. До сих пор добиться этого можно было, только используя отдельный AtomicInteger (что добавляет и расход памяти, и дополнительные проблемы конкурентности из-за косвенного доступа), в некоторых ситуациях — атомарные FieldUpdater (накладные расходы которых часто превышают стоимость самой операции) или небезопасный (а также непереносимый и неподдерживаемый) API sun.misc.Unsafe для интринсиков JVM. Интринсики работают быстрее, поэтому они стали широко использоваться в ущерб безопасности и переносимости.
Ожидается, что без этого JEP эти проблемы обострятся, когда атомарные API будут расширены для поддержки дополнительных политик согласованности доступа (соответствующих недавней модели памяти C++11) в рамках пересмотра Java Memory Model (модели памяти Java).
Описание
Variable handle — это типизированная ссылка на переменную, которая поддерживает чтение и запись переменной в различных режимах доступа. Поддерживаемые виды переменных: поля экземпляра, статические поля и элементы массивов. Рассматриваются и могут быть поддержаны другие виды переменных, например представления массивов (просмотр массива byte или char как массива long) и области памяти вне кучи, описываемые с помощью ByteBuffer.
Для variable handles нужны улучшения библиотек, улучшения JVM и поддержка в компиляторе. Кроме того, нужны небольшие изменения в Java Language Specification и Java Virtual Machine Specification. Рассматриваются также небольшие улучшения языка, которые усиливают проверку типов во время компиляции и дополняют существующий синтаксис.
Ожидается, что итоговые спецификации можно будет естественным образом расширить на дополнительные типы значений, подобные примитивным, или дополнительные типы, подобные массивам, если они когда-нибудь появятся в Java. Однако это не универсальный механизм транзакций для управления доступом и обновлением нескольких переменных. Альтернативные формы выражения и реализации таких конструкций могут быть исследованы в ходе работы над этим JEP и могут стать предметом дальнейших JEP.
Variable handles моделируются одним абстрактным классом java.lang.invoke.VarHandle, в котором каждый режим доступа к переменной представлен сигнатурно-полиморфным методом.
Набор режимов доступа представляет собой минимальный жизнеспособный набор и спроектирован так, чтобы быть совместимым с атомарными операциями C/C++11, не завися от пересмотра Java Memory Model. При необходимости будут добавлены дополнительные режимы доступа. Некоторые режимы доступа могут быть неприменимы к определённым типам переменных; в таком случае при вызове на соответствующем экземпляре VarHandle будет выброшено UnsupportedOperationException.
Режимы доступа делятся на следующие категории:
-
режимы доступа на чтение, например чтение переменной с эффектами упорядочивания памяти volatile;
-
режимы доступа на запись, например обновление переменной с эффектами упорядочивания памяти release;
-
режимы атомарного обновления, например compare-and-set над переменной с эффектами упорядочивания памяти volatile и для чтения, и для записи;
-
режимы числового атомарного обновления, например get-and-add с эффектами упорядочивания памяти plain для записи и acquire для чтения.
-
режимы побитового атомарного обновления, например get-and-bitwise-and с эффектами упорядочивания памяти release для записи и plain для чтения.
Последние три категории обычно называют режимами read-modify-write.
Сигнатурная полиморфность методов режимов доступа позволяет variable handles поддерживать множество видов и типов переменных с помощью всего одного абстрактного класса. Так удаётся избежать лавинообразного роста числа классов для отдельных видов и типов переменных. Кроме того, хотя сигнатуры методов режимов доступа объявлены как массив аргументов переменной длины типа Object, сигнатурная полиморфность гарантирует, что примитивные значения аргументов не будут упаковываться и аргументы не будут собираться в массив. Благодаря этому поведение и производительность во время выполнения предсказуемы для интерпретатора HotSpot и компиляторов C1/C2.
Методы создания экземпляров VarHandle находятся там же, где методы получения экземпляров MethodHandle, дающих доступ к эквивалентным или похожим видам переменных.
Методы создания экземпляров VarHandle для видов переменных — полей экземпляра и статических полей — находятся в java.lang.invoke.MethodHandles.Lookup; экземпляры создаются поиском поля в соответствующем классе-получателе. Например, такой поиск для получения VarHandle для поля с именем i типа int в классе-получателе Foo может выполняться так:
class Foo {
int i;
...
}
...
class Bar {
static final VarHandle VH_FOO_FIELD_I;
static {
try {
VH_FOO_FIELD_I = MethodHandles.lookup().
in(Foo.class).
findVarHandle(Foo.class, "i", int.class);
} catch (Exception e) {
throw new Error(e);
}
}
}
Поиск VarHandle, дающего доступ к полю, перед созданием и возвратом VarHandle выполнит точно те же проверки контроля доступа (от имени класса поиска), что и поиск MethodHandle, дающего доступ к тому же полю на чтение и запись (см. методы find{,Static}{Getter,Setter} в классе MethodHandles.Lookup).
Методы режимов доступа выбрасывают UnsupportedOperationException при вызове в следующих условиях:
-
Методы режимов доступа на запись для
VarHandleк final-полю. -
Методы числовых режимов доступа (
getAndAddиaddAndGet) для ссылочного типа переменной или нечислового типа (например,boolean). -
Методы побитовых режимов доступа для ссылочного типа переменной или для типов
floatиdouble(последнее ограничение может быть снято в будущей редакции)
Поле не обязательно помечать как volatile, чтобы соответствующий VarHandle мог выполнять volatile-доступ. Фактически модификатор volatile, если он есть, игнорируется. Это отличается от поведения java.util.concurrent.atomic.Atomic{Int, Long, Reference}FieldUpdater, где соответствующие поля должны быть помечены как volatile. Это может быть слишком строгим ограничением в тех случаях, когда известно, что некоторые volatile-обращения нужны не всегда.
Методы создания экземпляров VarHandle для типов переменных на основе массивов находятся в java.lang.invoke.MethodHandles (см. методы arrayElement{Getter, Setter} в классе MethodHandles). Например, VarHandle для массива int можно создать так:
VarHandle intArrayHandle = MethodHandles.arrayElementVarHandle(int[].class);
Методы режимов доступа выбрасывают UnsupportedOperationException при вызове в следующих условиях:
-
Методы числовых режимов доступа (
getAndAddиaddAndGet) для ссылочного типа компонента массива или нечислового типа (например,boolean) -
Методы побитовых режимов доступа для ссылочного типа переменной или для типов
floatиdouble(последнее ограничение может быть снято в будущей редакции)
Для видов переменных — полей экземпляра, статических полей и элементов массивов — в качестве типа переменной поддерживаются все примитивные и ссылочные типы. Другие виды переменных могут поддерживать все эти типы или их часть.
Методы создания экземпляров VarHandle для типов переменных на основе представлений массивов также находятся в java.lang.invoke.MethodHandles. Например, VarHandle для просмотра массива byte как невыровненного массива long можно создать так:
VarHandle longArrayViewHandle = MethodHandles.byteArrayViewVarHandle(
long[].class, java.nio.ByteOrder.BIG_ENDIAN);
Хотя подобных механизмов можно добиться с помощью java.nio.ByteBuffer, для этого нужно создать экземпляр ByteBuffer, оборачивающий массив byte. Это не всегда гарантирует надёжную производительность из-за хрупкости escape-анализа и того, что обращения должны проходить через экземпляр ByteBuffer. При невыровненном доступе все методы режимов доступа, кроме plain, будут выбрасывать IllegalStateException. При выровненном доступе, в зависимости от типа переменной, возможны некоторые volatile-операции. Такие экземпляры VarHandle можно использовать для векторизации доступа к массиву.
Число аргументов, их типы и тип возвращаемого значения методов режимов доступа определяются видом переменной, типом переменной и характеристиками режима доступа. Методы создания VarHandle (например, описанные выше) будут документировать эти требования. Например, compareAndSet на ранее найденном VH_FOO_FIELD_I требует 3 аргумента: экземпляр получателя Foo и два int для ожидаемого и фактического значений:
Foo f = ...
boolean r = VH_FOO_FIELD_I.compareAndSet(f, 0, 1);
Для сравнения, getAndSet требует 2 аргумента: экземпляр получателя Foo и один int — устанавливаемое значение:
int o = (int) VH_FOO_FIELD_I.getAndSet(f, 2);
Для доступа к элементам массива нужен дополнительный аргумент типа int между аргументом-получателем и аргументами-значениями (если они есть), соответствующий индексу элемента массива, над которым выполняется операция.
Для предсказуемого поведения и производительности во время выполнения экземпляры VarHandle следует хранить в полях static final (как требуется для экземпляров Atomic{Int, Long, Reference}FieldUpdater). Так гарантируется свёртка констант при вызовах методов режимов доступа, например устранение проверок сигнатуры метода и/или проверок приведения аргументов.
Примечание: будущие улучшения HotSpot могут поддержать свёртку констант для экземпляров
VarHandleилиMethodHandle, хранящихся в нестатических final-полях, аргументах методов или локальных переменных.
MethodHandle для метода режима доступа VarHandle можно получить с помощью MethodHandles.Lookup.findVirtual. Например, чтобы получить MethodHandle для режима доступа «compareAndSet» для определённого вида и типа переменной:
Foo f = ...
MethodHandle mhToVhCompareAndSet = MethodHandles.publicLookup().findVirtual(
VarHandle.class,
"compareAndSet",
MethodType.methodType(boolean.class, Foo.class, int.class, int.class));
Затем MethodHandle можно вызвать, передав первым параметром экземпляр VarHandle, совместимый по виду и типу переменной:
boolean r = (boolean) mhToVhCompareAndSet.invokeExact(VH_FOO_FIELD_I, f, 0, 1);
Или mhToVhCompareAndSet можно привязать к экземпляру VarHandle и затем вызвать:
MethodHandle mhToBoundVhCompareAndSet = mhToVhCompareAndSet
.bindTo(VH_FOO_FIELD_I);
boolean r = (boolean) mhToBoundVhCompareAndSet.invokeExact(f, 0, 1);
Такой поиск MethodHandle с помощью findVirtual выполняет преобразование asType для приведения аргументов и возвращаемых значений. Поведение эквивалентно MethodHandle, полученному с помощью MethodHandles.varHandleInvoker, аналога MethodHandles.invoker`:
MethodHandle mhToVhCompareAndSet = MethodHandles.varHandleExactInvoker(
VarHandle.AccessMode.COMPARE_AND_SET,
MethodType.methodType(boolean.class, Foo.class, int.class, int.class));
boolean r = (boolean) mhToVhCompareAndSet.invokeExact(VH_FOO_FIELD_I, f, 0, 1);
Таким образом, VarHandle можно использовать в сценариях со стиранием типов или с рефлексией через класс-обёртку, например заменив использование Unsafe в классах java.util.concurrent.Atomic*FieldUpdater/Atomic*Array. (Хотя требуется дальнейшая работа, чтобы updater-классы получили доступ к искомым полям в объявляющем классе.)
Компиляция исходного кода с вызовом метода режима доступа будет следовать тем же правилам, что и вызов сигнатурно-полиморфных методов MethodHandle.invokeExact и MethodHandle.invoke. В Java Language Specification потребуются следующие дополнения:
- Упомянуть сигнатурно-полиморфные методы режимов доступа в классе
VarHandle. - Разрешить сигнатурно-полиморфным методам возвращать типы, отличные от Object, что означает, что тип возвращаемого значения не полиморфный (иначе он объявлялся бы приведением в месте вызова). Так проще вызывать методы доступа на запись, возвращающие void, и вызывать
compareAndSet, возвращающий значениеboolean.
Было бы желательно, но не обязательно, улучшить компиляцию исходного кода с вызовом сигнатурно-полиморфного метода так, чтобы полиморфный тип возвращаемого значения выводился из целевого типа и явное приведение не требовалось.
Примечание: синтаксис и поддержка во время выполнения для поиска
MethodHandleилиVarHandleс использованием синтаксиса ссылок на методы, напримерVarHandle VH_FOO_FIELD_I = Foo::i, желательны, но выходят за рамки этого JEP.
Вызов метода режима доступа во время выполнения будет подчиняться правилам, аналогичным правилам вызова сигнатурно-полиморфных методов MethodHandle.invokeExact и MethodHandle.invoke. В Java Virtual Machine Specification потребуется внести следующие дополнения:
- Упомянуть сигнатурно-полиморфные методы режимов доступа в классе
VarHandle. - Специфицировать поведение байт-кода
invokevirtualпри вызове сигнатурно-полиморфных методов режимов доступа. Предполагается, что такое поведение можно специфицировать, определив преобразование вызова метода режима доступа вMethodHandle, который затем вызывается черезinvokeExactс теми же параметрами (см. предыдущее использованиеMethodHandles.Lookup.findVirtual).
Важно, чтобы реализации VarHandle для поддерживаемых видов переменных, типов и режимов доступа были стабильно эффективными и соответствовали целевым показателям производительности. Сигнатурно-полиморфные методы помогают избежать упаковки (boxing) и упаковки аргументов в массив. Реализации будут:
-
находиться в пакете
java.lang.invoke, final-поля классов которого HotSpot считает действительно неизменяемыми, что позволяет выполнять свёртку констант, когда самVarHandleхранится в поле static final; -
использовать внутренние аннотации JDK
@Stableдля свёртки констант, значения которых меняются только один раз, и@ForceInline, чтобы методы встраивались даже при достижении обычных порогов встраивания; и -
использовать
sun.misc.Unsafeдля нижележащего расширенного volatile-доступа.
Потребуется несколько интринсиков HotSpot, некоторые из них перечислены ниже:
- Интринсик для
Class.cast, который уже добавлен (см. JDK-8054492). До добавления этого интринсика после свёртки константClass.castоставались избыточные проверки, которые могли приводить к ненужным деоптимизациям.
-
Интринсик для режима доступа
acquire-get, который может синхронизироваться с интринсиком для режима доступаset-release(см.sun.misc.Unsafe.putOrdered{Int, Long, Object}) при конкурентном доступе к переменным. -
Интринсики для проверок границ массива JDK-8042997. Можно добавить статические методы
java.util.Arrays, которые выполняют такие проверки и принимают функцию, вызываемую при неудачной проверке, чтобы вернуть выбрасываемое исключение или строку сообщения, включаемую в выбрасываемое исключение. Такие интринсики позволяют лучше выполнять сравнения с использованием беззнаковых значений (поскольку длина массива всегда положительна) и лучше выносить проверки диапазона за пределы развёрнутых циклов по элементам массива.
Кроме того, в HotSpot уже реализованы (JDK-8073480) или требуются (JDK-8003585 — для упрощения (strength reduction) проверок диапазона, например во фреймворке fork/join или, например, в HashMap или ConcurrentHashMap) дальнейшие улучшения проверок диапазона.
Реализации VarHandle должны минимально зависеть от других классов пакета java.lang.invoke, чтобы не увеличивать время запуска и не допустить циклических зависимостей при статической инициализации. Например, ConcurrentHashMap используется такими классами, и если ConcurrentHashMap изменить так, чтобы он использовал VarHandles, нужно убедиться, что не появляются циклические зависимости. Возможны и другие, менее очевидные циклы — при использовании ThreadLocalRandom и того, как он использует AtomicInteger. Также желательно, чтобы время компиляции C2 в HotSpot не увеличивалось чрезмерно для методов, содержащих вызовы методов VarHandle.
Барьеры памяти
Операции с барьерами определены как статические методы класса VarHandle и представляют собой минимально достаточный набор для тонкого управления порядком операций с памятью.
/**
* Ensures that loads and stores before the fence will not be
* reordered with loads and stores after the fence.
*
* @apiNote Ignoring the many semantic differences from C and
* C++, this method has memory ordering effects compatible with
* atomic_thread_fence(memory_order_seq_cst)
*/
public static void fullFence() {}
/**
* Ensures that loads before the fence will not be reordered with
* loads and stores after the fence.
*
* @apiNote Ignoring the many semantic differences from C and
* C++, this method has memory ordering effects compatible with
* atomic_thread_fence(memory_order_acquire)
*/
public static void acquireFence() {}
/**
* Ensures that loads and stores before the fence will not be
* reordered with stores after the fence.
*
* @apiNote Ignoring the many semantic differences from C and
* C++, this method has memory ordering effects compatible with
* atomic_thread_fence(memory_order_release)
*/
public static void releaseFence() {}
/**
* Ensures that loads before the fence will not be reordered with
* loads after the fence.
*/
public static void loadLoadFence() {}
/**
* Ensures that stores before the fence will not be reordered with
* stores after the fence.
*/
public static void storeStoreFence() {}
Полный барьер сильнее (с точки зрения гарантий упорядочения), чем барьер acquire, который, в свою очередь, сильнее барьера load-load. Аналогично полный барьер сильнее барьера release, который сильнее барьера store-store.
Барьер достижимости
Барьер достижимости определён как статический метод java.lang.ref.Reference:
class java.lang.ref.Reference {
// add:
/**
* Ensures that the object referenced by the given reference
* remains <em>strongly reachable</em> (as defined in the {@link
* java.lang.ref} package documentation), regardless of any prior
* actions of the program that might otherwise cause the object to
* become unreachable; thus, the referenced object is not
* reclaimable by garbage collection at least until after the
* invocation of this method. Invocation of this method does not
* itself initiate garbage collection or finalization.
*
* @param ref the reference. If null, this method has no effect.
*/
public static void reachabilityFence(Object ref) {}
}
См. JDK-8133348.
Сейчас за рамками JEP остаётся аннотация, например @Finalized, объявляемая на методе, которая во время компиляции или выполнения давала бы такой результат, как если бы тело метода было обёрнуто следующим образом:
try {
<method body>
} finally {
Reference.reachabilityFence(this);
}
Предполагается, что такую функциональность можно поддержать с помощью обработчика аннотаций во время компиляции.
Альтернативы
Рассматривалось введение новых форм «value type», поддерживающих volatile-операции. Однако это противоречило бы свойствам других типов, а программистам было бы сложнее их использовать. Также рассматривалась опора на java.util.concurrent.atomic FieldUpdater, но из-за накладных расходов во время выполнения и ограничений в использовании они не подходят.
За многие годы обсуждения этих проблем предлагались и были отвергнуты как неработоспособные по синтаксическим соображениям, соображениям эффективности и/или удобства использования ещё несколько альтернатив, в том числе основанные на ссылках на поля.
Расширения синтаксиса рассматривались в предыдущей версии этого JEP, но были признаны слишком «магическими»: ключевое слово volatile перегружалось для ссылки на «плавающие» интерфейсы — один для ссылок и по одному для каждого поддерживаемого примитивного типа.
Обобщённые типы, наследующие VarHandle, рассматривались в предыдущей версии этого JEP, но такое дополнение с расширенными полиморфными сигнатурами для обобщённых типов и особой обработкой упакованных переменных типа было сочтено преждевременным с учётом будущего выпуска Java с value types и обобщениями над примитивами (JEP 218) и улучшенными массивами (Arrays 2.0).
В предыдущей версии этого JEP рассматривался также зависящий от реализации подход с invokedynamic. Он требовал тщательно согласовывать скомпилированные вызовы методов с invokedynamic и без него, чтобы их семантика совпадала. Кроме того, использование invokedynamic в базовых классах, например в ConcurrentHashMap, приведёт к циклическим зависимостям.
Тестирование
Стресс-тесты будут разработаны с помощью тестового окружения jcstress.
Риски и допущения
Производительность прототипа реализации VarHandle проверена на нанобенчмарках и бенчмарках fork/join, где использование sun.misc.Unsafe в библиотеке fork/join было заменено на VarHandle. Пока серьёзных проблем с производительностью не замечено, а выявленные проблемы компилятора HotSpot не выглядят трудными (свёртка проверок приведения типов и улучшение проверок границ массивов). Поэтому мы уверены, что этот подход осуществим. Однако мы ожидаем, что потребуется больше экспериментов, чтобы убедиться в надёжности методов компиляции в критичных к производительности контекстах, где эти конструкции нужнее всего.
Зависимости
Классы в java.util.concurrent (и в других выявленных частях JDK) будут переведены с sun.misc.Unsafe на VarHandle.
Этот JEP не зависит от JEP 188: Java Memory Model Update.