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

Путеводитель по sun.misc.Unsafe

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

1. Обзор

В этой статье мы рассмотрим увлекательный класс, предоставляемый JRE — Unsafe из пакета sun.misc . Этот класс предоставляет нам низкоуровневые механизмы, предназначенные для использования только основной библиотекой Java, а не обычными пользователями.

Это дает нам низкоуровневые механизмы, предназначенные в первую очередь для внутреннего использования в основных библиотеках.

2. Получение экземпляра небезопасного

Во-первых, чтобы иметь возможность использовать класс Unsafe , нам нужно получить экземпляр, что не так просто, учитывая, что класс был разработан только для внутреннего использования.

Способ получения экземпляра — через статический метод getUnsafe(). Предостережение в том, что по умолчанию это вызовет исключение SecurityException.

К счастью, мы можем получить экземпляр с помощью отражения:

Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);

3. Создание экземпляра класса с помощью Unsafe

Допустим, у нас есть простой класс с конструктором, который устанавливает значение переменной при создании объекта:

class InitializationOrdering {
private long a;

public InitializationOrdering() {
this.a = 1;
}

public long getA() {
return this.a;
}
}

Когда мы инициализируем этот объект с помощью конструктора, метод getA() вернет значение 1:

InitializationOrdering o1 = new InitializationOrdering();
assertEquals(o1.getA(), 1);

Но мы можем использовать метод allocateInstance () с помощью Unsafe. Он выделит память только для нашего класса и не вызовет конструктор:

InitializationOrdering o3 
= (InitializationOrdering) unsafe.allocateInstance(InitializationOrdering.class);

assertEquals(o3.getA(), 0);

Обратите внимание, что конструктор не был вызван, поэтому метод getA() вернул значение по умолчанию для типа long — 0.

4. Изменение личных полей

Допустим, у нас есть класс, который содержит секретное частное значение:

class SecretHolder {
private int SECRET_VALUE = 0;

public boolean secretIsDisclosed() {
return SECRET_VALUE == 1;
}
}

Используя метод putInt() из Unsafe, мы можем изменить значение частного поля SECRET_VALUE , изменяя/искажая состояние этого экземпляра:

SecretHolder secretHolder = new SecretHolder();

Field f = secretHolder.getClass().getDeclaredField("SECRET_VALUE");
unsafe.putInt(secretHolder, unsafe.objectFieldOffset(f), 1);

assertTrue(secretHolder.secretIsDisclosed());

Как только мы получим поле вызовом отражения, мы можем изменить его значение на любое другое значение int , используя метод Unsafe .

5. Создание исключения

Код, вызываемый через Unsafe , не проверяется компилятором так же, как обычный код Java. Мы можем использовать метод throwException() для создания любого исключения, не ограничивая вызывающую программу обработкой этого исключения, даже если это проверенное исключение:

@Test(expected = IOException.class)
public void givenUnsafeThrowException_whenThrowCheckedException_thenNotNeedToCatchIt() {
unsafe.throwException(new IOException());
}

После генерирования исключения IOException, которое проверяется, нам не нужно ни перехватывать его, ни указывать в объявлении метода.

6. Память вне кучи

Если приложению не хватает доступной памяти на JVM, мы можем в конечном итоге заставить процесс GC запускаться слишком часто. В идеале нам нужна особая область памяти, вне кучи и не контролируемая процессом GC.

Метод allocateMemory() из класса Unsafe дает нам возможность выделять огромные объекты из кучи, а это означает, что эта память не будет видна и принята во внимание сборщиком мусора и JVM .

Это может быть очень полезно, но мы должны помнить, что этой памятью нужно управлять вручную и правильно освобождать ее с помощью freeMemory() , когда она больше не нужна.

Допустим, мы хотим создать большой массив байтов памяти вне кучи. Для этого мы можем использовать метод allocateMemory() :

class OffHeapArray {
private final static int BYTE = 1;
private long size;
private long address;

public OffHeapArray(long size) throws NoSuchFieldException, IllegalAccessException {
this.size = size;
address = getUnsafe().allocateMemory(size * BYTE);
}

private Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
return (Unsafe) f.get(null);
}

public void set(long i, byte value) throws NoSuchFieldException, IllegalAccessException {
getUnsafe().putByte(address + i * BYTE, value);
}

public int get(long idx) throws NoSuchFieldException, IllegalAccessException {
return getUnsafe().getByte(address + idx * BYTE);
}

public long size() {
return size;
}

public void freeMemory() throws NoSuchFieldException, IllegalAccessException {
getUnsafe().freeMemory(address);
}
}

В конструкторе OffHeapArray мы инициализируем массив заданного размера. Мы сохраняем начальный адрес массива в поле адреса . Метод set() принимает индекс и заданное значение , которое будет храниться в массиве. Метод get() извлекает значение байта, используя его индекс, который является смещением от начального адреса массива.

Затем мы можем выделить этот массив вне кучи, используя его конструктор:

long SUPER_SIZE = (long) Integer.MAX_VALUE * 2;
OffHeapArray array = new OffHeapArray(SUPER_SIZE);

Мы можем поместить N значений байтов в этот массив, а затем извлечь эти значения, суммируя их, чтобы проверить, правильно ли работает наша адресация:

int sum = 0;
for (int i = 0; i < 100; i++) {
array.set((long) Integer.MAX_VALUE + i, (byte) 3);
sum += array.get((long) Integer.MAX_VALUE + i);
}

assertEquals(array.size(), SUPER_SIZE);
assertEquals(sum, 300);

В конце нам нужно освободить память обратно в ОС, вызвав freeMemory().

7. Операция сравнения и замены

Очень эффективные конструкции из пакета java.concurrent , такие как AtomicInteger, используют методы compareAndSwap() из Unsafe , чтобы обеспечить максимально возможную производительность. Эта конструкция широко используется в алгоритмах блокировки без блокировки, которые могут использовать инструкции процессора CAS для обеспечения значительного ускорения по сравнению со стандартным механизмом пессимистической синхронизации в Java.

Мы можем создать счетчик на основе CAS, используя метод compareAndSwapLong() из Unsafe :

class CASCounter {
private Unsafe unsafe;
private volatile long counter = 0;
private long offset;

private Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
return (Unsafe) f.get(null);
}

public CASCounter() throws Exception {
unsafe = getUnsafe();
offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
}

public void increment() {
long before = counter;
while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
before = counter;
}
}

public long getCounter() {
return counter;
}
}

В конструкторе CASCounter мы получаем адрес поля счетчика, чтобы потом использовать его в методе increment() . Это поле должно быть объявлено как volatile, чтобы его могли видеть все потоки, записывающие и считывающие это значение. Мы используем метод objectFieldOffset() для получения адреса памяти поля смещения .

Наиболее важной частью этого класса является метод increment() . Мы используем compareAndSwapLong() в цикле while для увеличения ранее извлеченного значения, проверяя, изменилось ли это предыдущее значение с момента его извлечения.

Если да, то мы повторяем эту операцию до тех пор, пока не добьемся успеха. Здесь нет блокировки, поэтому этот алгоритм называется lock-free.

Мы можем протестировать наш код, увеличив общий счетчик из нескольких потоков:

int NUM_OF_THREADS = 1_000;
int NUM_OF_INCREMENTS = 10_000;
ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS);
CASCounter casCounter = new CASCounter();

IntStream.rangeClosed(0, NUM_OF_THREADS - 1)
.forEach(i -> service.submit(() -> IntStream
.rangeClosed(0, NUM_OF_INCREMENTS - 1)
.forEach(j -> casCounter.increment())));

Затем, чтобы подтвердить правильность состояния счетчика, мы можем получить от него значение счетчика:

assertEquals(NUM_OF_INCREMENTS * NUM_OF_THREADS, casCounter.getCounter());

8. Парковка/разпарковка

В Unsafe API есть два интересных метода, которые используются JVM для переключения потоков контекста. Когда поток ожидает какого-либо действия, JVM может заблокировать этот поток, используя метод park() из класса Unsafe .

Он очень похож на метод Object.wait() , но вызывает собственный код ОС, таким образом, используя некоторые особенности архитектуры для достижения наилучшей производительности.

Когда поток блокируется и его нужно снова сделать работоспособным, JVM использует метод unpark() . Мы часто будем видеть вызовы этих методов в дампах потоков, особенно в приложениях, использующих пулы потоков.

9. Заключение

В этой статье мы рассмотрели класс Unsafe и его наиболее полезные конструкции.

Мы увидели, как получить доступ к закрытым полям, как выделить память вне кучи и как использовать конструкцию сравнения и замены для реализации алгоритмов без блокировок.

Реализацию всех этих примеров и фрагментов кода можно найти на GitHub — это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.