Руководство по sun.misc.Unsafe

Руководство поsun.misc.Unsafe

1. обзор

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

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

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

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

The way to obtain the instance is via the static method getUnsafe(). Предостережение в том, что по умолчанию - это вызоветSecurityException.

К счастью,we can obtain the instance using reflection:

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. Изменение частных полей

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

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 дает нам возможность выделять огромные объекты из кучи, что означает, чтоthis memory will not be seen and taken into account by the GC and the 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, мы инициализируем массив с заданнымsize.. Мы сохраняем начальный адрес массива в полеaddress. Методset() принимает индекс и заданныйvalue, который будет храниться в массиве. Метод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. CompareAndSwap Работа

Очень эффективные конструкции из пакета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(), чтобы получить адрес памяти поляoffset.

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

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

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

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. Park/Unpark

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

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

When the thread is blocked and needs to be made runnable again, the JVM uses the unpark() method. Мы часто видим эти вызовы методов в дампах потоков, особенно в приложениях, использующих пулы потоков.

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

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

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

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