1. Введение
Java 9 привнесла ряд новых полезных функций для разработчиков.
Одним из них является API-интерфейс java.lang.invoke.VarHandle
, представляющий дескрипторы переменных, который мы собираемся изучить в этой статье.
2. Что такое дескрипторы переменных?
Как правило, дескриптор переменной — это просто типизированная ссылка на переменную . Переменная может быть элементом массива, экземпляром или статическим полем класса.
Класс VarHandle
обеспечивает доступ для записи и чтения к переменным при определенных условиях.
VarHandles
неизменяемы и не имеют видимого состояния. Более того, они не могут быть подклассифицированы.
Каждый VarHandle
имеет:
- общий тип
T
, который является типом каждой переменной, представленной этимVarHandle
- список типов координат
CT
, которые являются типами координатных выражений, которые позволяют найти переменную, на которую ссылается этотVarHandle
Список типов координат может быть пустым.
Цель VarHandle
— определить стандарт для вызова эквивалентов операций java
.util.concurrent.atomic
и sun.misc.Unsafe
с полями и элементами массива.
Эти операции в основном являются атомарными или упорядоченными операциями — например, приращение атомарного поля.
3. Создание дескрипторов переменных
Чтобы использовать VarHandle
, нам сначала нужны переменные.
Давайте объявим простой класс с разными переменными типа int
, которые мы будем использовать в наших примерах:
public class VariableHandlesUnitTest {
public int publicTestVariable = 1;
private int privateTestVariable = 1;
public int variableToSet = 1;
public int variableToCompareAndSet = 1;
public int variableToGetAndAdd = 0;
public byte variableToBitwiseOr = 0;
}
3.1. Руководящие принципы и соглашения
По соглашению мы должны объявить VarHandle
как статические конечные
поля и явно инициализировать их в статических блоках. Кроме того, мы обычно используем версию имени соответствующего поля в верхнем регистре в качестве их имени.
Например, вот как сама Java использует VarHandle
внутри для реализации AtomicReference
:
private volatile V value;
private static final VarHandle VALUE;
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
VALUE = l.findVarHandle(AtomicReference.class, "value", Object.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
В большинстве случаев мы можем использовать тот же шаблон при использовании VarHandle
s.
Теперь, когда мы это знаем, давайте двигаться дальше и посмотрим, как мы можем использовать их на практике.
3.2. Дескрипторы переменных для общедоступных переменных
Теперь мы можем получить VarHandle
для нашей publicTestVariable,
используя метод findVarHandle ()
:
VarHandle PUBLIC_TEST_VARIABLE = MethodHandles
.lookup()
.in(VariableHandlesUnitTest.class)
.findVarHandle(VariableHandlesUnitTest.class, "publicTestVariable", int.class);
assertEquals(1, PUBLIC_TEST_VARIABLE.coordinateTypes().size());
assertEquals(VariableHandlesUnitTest.class, PUBLIC_TEST_VARIABLE.coordinateTypes().get(0));
Мы видим, что свойство координаты
Types этого VarHandle
не пусто и имеет один элемент, который является нашим классом VariableHandlesUnitTest .
3.3. Дескрипторы переменных для частных переменных
Если у нас есть закрытый член и нам нужен дескриптор переменной для такой переменной, мы можем получить его с помощью метода privateLookupIn()
:
VarHandle PRIVATE_TEST_VARIABLE = MethodHandles
.privateLookupIn(VariableHandlesUnitTest.class, MethodHandles.lookup())
.findVarHandle(VariableHandlesUnitTest.class, "privateTestVariable", int.class);
assertEquals(1, PRIVATE_TEST_VARIABLE.coordinateTypes().size());
assertEquals(VariableHandlesUnitTest.class, PRIVATE_TEST_VARIABLE.coordinateTypes().get(0));
Здесь мы выбрали метод privateLookupIn()
, который имеет более широкий доступ, чем обычный lookup()
. Это позволяет нам получить доступ к закрытым
, общедоступным
или защищенным
переменным.
До Java 9 эквивалентным API для этой операции был класс Unsafe и метод
setAccessible()
из Reflection
API.
Однако этот подход имеет свои недостатки. Например, это будет работать только для конкретного экземпляра переменной.
VarHandle
— лучшее и быстрое решение в таких случаях.
3.4. Дескрипторы переменных для массивов
Мы могли бы использовать предыдущий синтаксис для получения полей массива.
Однако мы также можем получить VarHandle
для массива определенного типа:
VarHandle arrayVarHandle = MethodHandles.arrayElementVarHandle(int[].class);
assertEquals(2, arrayVarHandle.coordinateTypes().size());
assertEquals(int[].class, arrayVarHandle.coordinateTypes().get(0));
Теперь мы можем видеть, что такой VarHandle
имеет два типа координат int
и []
, которые представляют собой массив примитивов типа int .
4. Вызов методов VarHandle
Большинство методов VarHandle
ожидают переменное количество аргументов типа Object.
Использование Object…
в качестве аргумента отключает статическую проверку аргументов.
Вся проверка аргументов выполняется во время выполнения. Кроме того, разные методы предполагают наличие разного количества аргументов разных типов.
Если мы не сможем предоставить правильное количество аргументов с правильными типами, вызов метода вызовет исключение WrongMethodTypeException
.
Например, get()
ожидает по крайней мере один аргумент, который помогает найти переменную, а set()
ожидает еще один аргумент, который является значением, которое будет присвоено переменной.
5. Режимы доступа к переменным
Как правило, все методы класса VarHandle
относятся к пяти различным режимам доступа.
Давайте рассмотрим каждый из них в следующих подразделах.
5.1. Доступ для чтения
Методы с уровнем доступа на чтение позволяют получить значение переменной при заданных эффектах упорядочения памяти. Существует несколько методов с этим режимом доступа, таких как: get()
, getAcquire()
, getVolatile()
и getOpaque()
.
Мы можем легко использовать метод get()
для нашего VarHandle
:
assertEquals(1, (int) PUBLIC_TEST_VARIABLE.get(this));
Метод get()
принимает в качестве параметров только CoordinateTypes
, поэтому мы можем просто использовать его
в нашем случае.
5.2. Доступ для записи
Методы с уровнем доступа для записи позволяют нам устанавливать значение переменной при определенных эффектах упорядочения памяти.
Аналогично методам с доступом на чтение, у нас есть несколько методов с доступом на запись: set()
, setOpaque()
, setVolatile()
и setRelease()
.
Мы можем использовать метод set()
для нашего VarHandle
:
VARIABLE_TO_SET.set(this, 15);
assertEquals(15, (int) VARIABLE_TO_SET.get(this));
Метод set()
принимает как минимум два аргумента. Первый поможет найти переменную, а второй — это значение, которое будет установлено для переменной.
5.3. Доступ к атомарному обновлению
Методы с этим уровнем доступа можно использовать для атомарного обновления значения переменной.
Давайте воспользуемся методом compareAndSet()
, чтобы увидеть эффект:
VARIABLE_TO_COMPARE_AND_SET.compareAndSet(this, 1, 100);
assertEquals(100, (int) VARIABLE_TO_COMPARE_AND_SET.get(this));
Помимо CoordinateTypes
, метод compareAndSet()
принимает два дополнительных значения: oldValue
и newValue
. Метод устанавливает значение переменной, если оно было равно oldVariable,
или оставляет его без изменений в противном случае.
5.4. Доступ к числовому атомарному обновлению
Эти методы позволяют выполнять числовые операции, такие как getAndAdd
(), при определенных эффектах упорядочения памяти.
Давайте посмотрим, как мы можем выполнять атомарные операции с помощью VarHandle
:
int before = (int) VARIABLE_TO_GET_AND_ADD.getAndAdd(this, 200);
assertEquals(0, before);
assertEquals(200, (int) VARIABLE_TO_GET_AND_ADD.get(this));
Здесь метод getAndAdd()
сначала возвращает значение переменной, а затем добавляет предоставленное значение.
5.5. Доступ к побитовому атомарному обновлению
Методы с этим доступом позволяют нам атомарно выполнять побитовые операции при определенных эффектах упорядочения памяти.
Давайте посмотрим на пример использования метода getAndBitwiseOr()
:
byte before = (byte) VARIABLE_TO_BITWISE_OR.getAndBitwiseOr(this, (byte) 127);
assertEquals(0, before);
assertEquals(127, (byte) VARIABLE_TO_BITWISE_OR.get(this));
Этот метод получит значение нашей переменной и выполнит над ней операцию побитового ИЛИ.
Вызов метода вызовет исключение IllegalAccessException
, если он не сможет сопоставить режим доступа, требуемый методом, с режимом, разрешенным переменной.
Например, это произойдет, если мы попытаемся использовать метод set() для
конечной
переменной.
6. Эффекты упорядочения памяти
Ранее мы упоминали, что методы VarHandle
позволяют получить доступ к переменным при определенных эффектах упорядочения памяти.
Для большинства методов есть 4 эффекта упорядочения памяти:
Обычные
операции чтения и записи гарантируют побитовую атомарность для ссылок и примитивов до 32 бит. Кроме того, они не налагают ограничений на порядок по отношению к другим признакам.Непрозрачные
операции являются побитовыми атомарными и когерентно упорядочены в отношении доступа к одной и той же переменной.Операции Acquire
иRelease
подчиняются свойствамOpaque .
Кроме того,чтение
с получением будет упорядочено только после того, как будет совпадать с записью в режимеосвобождения .
Волатильные
операции полностью упорядочены по отношению друг к другу.
Очень важно помнить, что режимы доступа переопределяют предыдущие эффекты упорядочения памяти . Это означает, что, например, если мы используем get()
, это будет обычная операция чтения, даже если мы объявили нашу переменную как volatile
.
Из-за этого разработчики должны проявлять крайнюю осторожность при использовании операций VarHandle
.
7. Заключение
В этом уроке мы представили дескрипторы переменных и то, как их использовать.
Эта тема довольно сложна, поскольку дескрипторы переменных предназначены для низкоуровневых манипуляций, и их не следует использовать без необходимости.
Как всегда, образцы кода доступны на GitHub .