1. введение
Java 9 принес ряд новых полезных функций для разработчиков.
Одним из них является java.lang.invoke.VarHandle API – представление дескрипторов переменных, которые мы рассмотрим в этой статье.
2. Что Такое Переменные Дескрипторы?
Как правило, дескриптор переменной-это просто типизированная ссылка на переменную . Переменная может быть элементом массива, экземпляром или статическим полем класса.
То Вархандл класс обеспечивает доступ на запись и чтение к переменным при определенных условиях.
Дескрипторы Var являются неизменяемыми и не имеют видимого состояния. Более того, они не могут быть разделены на подклассы.
Каждый Var-дескриптор имеет:
- универсальный тип T , который является типом каждой переменной, представленной этим дескриптором Var
- список типов координат CT , которые являются типами выражений координат, которые позволяют найти переменную, на которую ссылается этот VarHandle
Список типов координат может быть пустым.
Цель VarHandle состоит в том, чтобы определить стандарт для вызова эквивалентов | java . util.concurrent.atomic и sun.misc.Unsafe | операций над полями и элементами массива.
Эти операции в основном являются атомарными или упорядоченными операциями — например, приращение атомарного поля.
3. Создание дескрипторов переменных
Чтобы использовать Var-дескриптор , сначала нам нужны переменные.
Давайте объявим простой класс с различными переменными типа 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. Руководящие принципы и конвенции
В качестве конвенции мы должны заявить: Вархандл s как статический финал поля и явно инициализировать их в статических блоках. Кроме того, мы обычно используем заглавную версию соответствующего имени поля в качестве их имени.
Например, вот как сама Java использует VarHandle s внутренне для реализации 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 для нашей публичной тестовой переменной с помощью find Var Handle() метод :
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));
Мы видим, что свойство coordinate Types этого VarHandle не является пустым и имеет один элемент, который является нашим классом VariableHandlesUnitTest .
3.3. Дескрипторы переменных для частных переменных
Если у нас есть закрытый член и нам нужен дескриптор переменной для такой переменной, мы можем получить это с помощью частного поиска В() методе :
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));
Здесь мы выбрали метод private Lookup In () , который имеет более широкий доступ, чем обычный lookup() . Это позволяет нам получить доступ к переменным private , public или protected .
До Java 9 эквивалентным API для этой операции был класс Unsafe и метод setAccessible() из API Reflection .
Однако такой подход имеет свои недостатки. Например, он будет работать только для конкретного экземпляра переменной.
Var Handle – лучшее и более быстрое решение в таких случаях.
3.4. Дескрипторы переменных для массивов
Мы могли бы использовать предыдущий синтаксис для получения полей массива.
Однако мы также можем получить дескриптор Var для массива определенного типа:
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. Использование Объекта… в качестве аргумента отключает статическую проверку аргументов.
Вся проверка аргументов выполняется во время выполнения. Кроме того, разные методы ожидают, что у них будет разное количество аргументов разных типов.
Если мы не сможем дать правильное количество аргументов с правильными типами, вызов метода вызовет исключение WrongMethodTypeException .
Например, get() ожидает по крайней мере один аргумент, который помогает найти переменную, но set() ожидает еще один аргумент, который является значением, которое должно быть присвоено переменной.
5. Переменные Режимы Доступа К Ручкам
Как правило, все методы класса VarHandle относятся к пяти различным режимам доступа.
Давайте рассмотрим каждый из них в следующих подразделах.
5.1. Доступ для чтения
Методы с уровнем доступа к чтению позволяют получить значение переменной при заданных эффектах упорядочения памяти. Существует несколько методов с этим режимом доступа, таких как: get() , get Acquire() , get Volatile() и get Opaque() .
Мы можем легко использовать метод get() на нашем VarHandle :
assertEquals(1, (int) PUBLIC_TEST_VARIABLE.get(this));
Метод get() принимает в качестве параметров только Типы координат , поэтому мы можем просто использовать это в нашем случае.
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));
Помимо типов координат , метод compareAndSet() принимает два дополнительных значения: oldValue и newValue . Метод устанавливает значение переменной, если оно было равно старой переменной или оставляет его неизменным в противном случае.
5.4. Доступ к Числовому Атомарному Обновлению
Эти методы позволяют выполнять числовые операции, такие как getAndAdd (), при определенных эффектах упорядочения памяти.
Давайте посмотрим, как мы можем выполнять атомарные операции с помощью дескриптора Var :
int before = (int) VARIABLE_TO_GET_AND_ADD.getAndAdd(this, 200); assertEquals(0, before); assertEquals(200, (int) VARIABLE_TO_GET_AND_ADD.get(this));
Здесь метод get И Add() сначала возвращает значение переменной, а затем добавляет предоставленное значение.
5.5. Побитовый Доступ К атомарному Обновлению
Методы с таким доступом позволяют нам атомарно выполнять побитовые операции при определенных эффектах упорядочения памяти.
Давайте рассмотрим пример использования метода get и побитового Или() :
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() для переменной final .
6. Эффекты упорядочения памяти
Ранее мы упоминали, что методы VarHandle допускают доступ к переменным при определенных эффектах упорядочения памяти.
Для большинства методов существует 4 эффекта упорядочения памяти:
- Простое чтение и запись гарантируют побитовую атомарность для ссылок и примитивов до 32 бит. Кроме того, они не накладывают никаких ограничений на порядок в отношении других признаков.
- Непрозрачные операции являются побитовыми атомарными и когерентно упорядоченными по отношению к доступу к одной и той же переменной.
- Получить и Освободить операции подчиняются Непрозрачным свойствам. Кроме того, Acquire reads будет заказан только после сопоставления Release mode записи.
- Изменчивые операции полностью упорядочены по отношению друг к другу.
Очень важно помнить, что режимы доступа будут переопределять предыдущие эффекты упорядочения памяти . Это означает, что, например , если мы используем get () , это будет простая операция чтения, даже если мы объявили нашу переменную как volatile .
Из-за этого разработчики должны проявлять крайнюю осторожность при использовании Var-дескриптора операций.
7. Заключение
В этом уроке мы представили дескрипторы переменных и способы их использования.
Эта тема довольно сложна, поскольку переменные дескрипторы предназначены для низкоуровневых манипуляций, и их не следует использовать без необходимости.
Как всегда, примеры кода доступны на GitHub .