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, поэтому его должно быть легко импортировать и запускать как есть.