Перейти к основному содержимому

Демистификация дескрипторов переменных в Java 9

· 7 мин. чтения

Задача: Наибольшая подстрока без повторений

Для заданной строки s, найдите длину наибольшей подстроки без повторяющихся символов. Подстрока — это непрерывная непустая последовательность символов внутри строки...

ANDROMEDA 42

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 .