Рубрики
Без рубрики

Путеводитель по солнцу.разное. Небезопасно

Узнайте, как воспользоваться преимуществами интересных методов sun.misc.Unsafe, которые выходят за рамки обычного использования Java

Автор оригинала: baeldung.

1. Обзор

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

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

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

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

Получить экземпляр можно с помощью статического метода get Unsafe(). Предостережение заключается в том, что по умолчанию – это вызовет исключение SecurityException.

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

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

3. Создание экземпляра класса С использованием Небезопасного

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

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 private:

class SecretHolder {
    private int SECRET_VALUE = 0;

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

Используя метод putInt() из Unsafe, мы можем изменить значение поля private 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. Создание исключения

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

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

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

6. Память Вне Кучи

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

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

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

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

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);
    }
}

В конструкторе массива Off Heap мы инициализируем массив заданного размера|/. Мы сохраняем начальный адрес массива в поле адрес . Метод 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);

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

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;
    }
}

В конструкторе CASE Counter мы получаем адрес поля счетчика, чтобы иметь возможность использовать его позже в методе increment () . Это поле должно быть объявлено как изменчивое, чтобы быть видимым для всех потоков, которые записывают и читают это значение. Мы используем метод 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. Парк/Unpark

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

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

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

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

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

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

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