Unsafe продолжает жить в Java 9
С началом работы над Java 9 было анонсировано удаление критически важных классов из пакетов sun.* (понятное дело Sun, а в последствии и Oracle заявляли, что их использование является собственным риском компаний и проектов), что вызвало шквал критики и недовольства со стороны сообщества (ибо highload решения для которых производительность это все, используют скрытые возможности sun.*). Предыстория началась 15 лет назад с выходом версии языка 1.4, за это время большое количество библиотек, фреймворков, приложений успели внедрить закрытый код в свой.
Вот только не полный перечень проектов, которые у всех на слуху: Scala, Kafka, Akka, Hadoop, Cassandra, Hazlecast и прочие…
Вроде бы новости без году неделя, однако постоянно приходится сталкиваться c тем, что люди не в курсе, и ожидают диких проблем с новым API в java 9 (может быть конечно и не без этого, однако. )
Реальность
Как ни странно, разработчики как из open jdk, так и из Oracle пошли на встречу сообществу и был принят документ JEP 260, если в двух словах: то решено оставить некоторые критически важные и широко используемые внутренние API, для которых пока не существует замены, однако это не означает, что будет гарантироваться их совместимость.
Таким образом решено оставить следующее API:
обеспечивает поддержку работы с сигналами (фактически IRC), может сообщить асинхронное событие вне JVM.
используется для работы непосредственно с памятью (обещают, что частично можно будет воспользоваться API из JEP 193: Variable Handles, также поговаривают что часть операций будет более эффективными).
говорит нам, какие классы находятся в стеке вызов (с выходом java 9, частично возможен переход на JEP 259: Stack-Walking API)
фактически master фабрика для создания объектов reflection
Не критическое API, которому уже существует замена, будет помечен как @Deprecated, уже начиная с Java 9, а начиная с Java 10 выводится из языка.
Уже сейчас доступна JDK9 Early Access на 24.02.2017 последний билд b158 (jdk9.java.net/download, jdk9.java.net/jigsaw) и если заглянуть внутрь, то можно обнаружить все выше перечисленные классы в модуле jdk.unsupported, также вместе с ними найдены:
com.sun.nio.file.ExtendedCopyOption
com.sun.nio.file.ExtendedOpenOption
com.sun.nio.file.ExtendedWatchEventModifier
com.sun.nio.file.SensitivityWatchEventModifier
обеспечивающие дополнительные возможности при работе с i/o-операциями.
Итого
Производители прислушались к сообществу, и в результате переход с пакетов sun.* обещает быть плавным и не критичным, а в случае проблем, можно спокойно спать и с 8 версией, буду рад ответить на вопросы (b.lutovich@socmetr.ru).
Небезопасный android часть 1: эксперименты с sun.misc.Unsafe
Java очень глубоко интегрирована в android и имеет в данной ОС свою нестандартную виртуальную машину — DVM/ART, поэтому многие детали реализации отличаются от привычных. А что насчёт внутреннего API sun.misc.Unsafe? В этом цикле статей с его помощью мы попытаемся максимально сломать виртуальную машину андроида.
Содержание
Часть 1. Введение. Создание arrayCast и его применение.
Часть 2. Классы-двойники. Получение списка всех полей, методов и конструкторов класса. Конвертация конструкторов в методы. Статический конструктор.
Введение
Начать стоит с того, что данный класс из себя представляет и для чего обычно используется. sun.misc.Unsafe существует с очень ранних версий java и необходим для выполнения действий, которые не предусмотрены языком, а их реализация в нативном коде по каким-то причинам нежелательна. Какие же возможности он предоставляет?
- Object allocateInstance(Class cls) — создание объекта, без вызова конструктора. Все поля заполняются стандартными значениями (0, 0.0, null и т.п.)
- XXX getXXX(Object obj, long offset) — чтение памяти, используя объект как указатель. Если он равен null, то смещение выступает в качестве нативного адреса
- void setXXX(Object obj, long offset, XXX value) — запись в память по тем же правилам, что и при чтении
- int arrayBaseOffset(Class cls), int arrayIndexScale(Class cls) — определение параметров массива (смещение от начала массива до первого элемента и размер элементов соответственно)
- long allocateMemory(long bytes), void freeMemory(long address) — обёртки над malloc и free из Си — выделение и освобождение памяти вне кучи
- void throwException(Throwable th) — «скрытное» бросание исключений (без их объявления в throws). Увы, но данный метод отсутствует в Unsafe андроида, поэтому придётся создавать его самостоятельно
- А так же куча других полезных вещей, особенно для многопоточного программирования, которые в данный момент нам не нужны
Подготовка
Как получить доступ ко всему этому инструментарию? Класс Unsafe содержит публичный метод getUnsafe, только вот незадача — в нём есть проверка безопасности:
public final class Unsafe < private static final Unsafe theUnsafe = new Unsafe(); @CallerSensitive public static Unsafe getUnsafe() < Classcaller = Reflection.getCallerClass(); // Этот код не даёт получить экземпляр ползовательским классам if (!VM.isSystemDomainLoader(caller.getClassLoader())) throw new SecurityException("Unsafe"); return theUnsafe; > >
Один из способов обойти её — прямой доступ к статическому полю theUnsafe через рефлексию
public static Unsafe getUnsafe() < try < // получаем поле Field field = Unsafe.class.getDeclaredField("theUnsafe"); // делаем его доступным для использования field.setAccessible(true); // получаем экземпляр sun.misc.Unsafe return (Unsafe) field.get(null); >catch (Exception e) < throw new RuntimeException(e); >>
Этот код настолько часто используется в разнообразных библиотеках, что его можно назвать классическим. Он даже не вызывает предупреждений во время выполнения (а вот доступ к конструктору вместо поля — ещё как).
Теперь нужно восполнить отсутствие метода throwException с помощью сломанного класса Thrower — мы создадим в нём метод с нужной сигнатурой, и насильно удалим throws Throwable:
Вариант кода на Smali — ассемблере Dalvik:
.class public Lcom/v7878/Thrower; .super Ljava/lang/Object; .method public constructor ()V .registers 1 invoke-direct < p0 >, Ljava/lang/Object;->()V return-void .end method .method public static throwException(Ljava/lang/Throwable;)V .registers 1 throw p0 .end method
То же самое, но на java:
// Не скомпилируется,так как нет throws Throwable public class Thrower < public static void throwException(Throwable th) < throw th; >>
Эксперименты
Время перейти к самой интересной части — экспериментам.
arrayCast
Что произойдёт если мы попробуем обратиться к объекту класса A как к объекту класса B, например попытаемся вызвать метод или получить поле, которого у него нет? Язык защищает от таких поступков ещё на этапе компиляции, и не даёт нам творить беспредел, но ведь теперь мы можем это игнорировать, да? Обмануть компилятор (а заодно и верификатор байт-кода) можно положив объект в поле неправильного типа и обращаясь уже к нему:
// Смещение от начала массива до первого элемента // int ARRAY_OBJECT_BASE_OFFSET = arrayBaseOffset(Object[].class); class A < public int a; >class B < public int b; >A obj = new A(); obj.a = 100; B[] array = new B[1]; // array[0] = obj; putObject(array, ARRAY_OBJECT_BASE_OFFSET, obj); System.out.println(array[0].b); // System.out: 100
В данном примере мы положили объект типа A (со значением 100 в поле a) как первый элемент массива B и получили значение 100 из поля b. Как это произошло?
Объект java представляет собой ссылку на память в куче, где лежат его данные — поля. Каждое поле имеет своё смещение от начала объекта (они отсортированы по уменьшению размера и по алфавиту. Сначала идут поля суперкласса). Например класс java.lang.Object имеет всего 2 поля:
public class Object < private transient Class shadow$_klass_; private transient int shadow$_monitor_; >
shadow$_klass_ содержит тип объекта, а shadow$_monitor_ данные монитора (используется для методов wait* и notify* и блоков synchronized) и/или кешированый хешкод. Первое поле будет иметь смещение 0 и размер 4 (размер поля не примитивного типа на андроиде равен 4 (даже на 64-битных устройствах!)), второе поле — смещение 4 и такой же размер.
Переходя к изначальному примеру — поле a типа A имеет то же самое смещение, что и поле b типа B, поэтому доступ ко второму даёт первое. Обратите внимание, что ни один объект не сменил реальный тип.
Теперь можно обобщить полученный опыт и сделать отдельный метод, выполняющий функцию оператора reinterpret_cast из C++
// Смещение от начала массива до первого элемента // int ARRAY_OBJECT_BASE_OFFSET = arrayBaseOffset(Object[].class); // размер элемента массива объектов (на андроиде всегда должен быть 4) // int ARRAY_OBJECT_INDEX_SCALE = arrayIndexScale(Object[].class); public T[] arrayCast(Class clazz, Object. data) < // нам нужен массив объектов, а не чего-то ещё if(clazz.isPrimitive()) < throw new IllegalArgumentException(); >// создаём массив типа T T[] out = (T[]) Array.newInstance(clazz, data.length); // переносим все объекты в массив for (int i = 0; i < data.length; i++) < putObject(out, ARRAY_OBJECT_BASE_OFFSET + i * ARRAY_OBJECT_INDEX_SCALE, data[i]); >return out; >
Свежеиспечённый метод принимает класс, к которому мы хотим привести группу объектов, и сами объекты, кладёт их в массив нужного типа и возвращает. Удивительно, но андроид ни разу не проверяет, что именно лежит в массиве, поэтому всё проходит гладко.
Теперь давайте применим arrayCast для изменения private final поля, чего нельзя сделать с помощью обычной рефлексии:
class A < private final int value; public A(int value) < this.value = value; >@Override public String toString() < return Integer.toString(value); >> class B < public int value; >A obj = new A(100); // приводим obj к типу B B[] array = arrayCast(B.class, obj); // меняем значение array[0].value = -1; // выводим в консоль System.out.println(obj); // System.out: -1
Получается, что при известном строении объекта с его полями можно творить что угодно, но как насчёт методов?
class A < private final int value; public A(int value) < this.value = value; >@Override public String toString() < return Integer.toString(value); >> class B < public int value; public void set(int x) < value = x; >> A obj = new A(100); // приводим obj к типу B B[] array = arrayCast(B.class, obj); // меняем значение array[0].set(-1); // выводим в консоль System.out.println(obj); // ожидаем получить: System.out: -1 // получаем: zygote A [runtime.cc:492] Runtime aborting. // и огромный столб текста с аварийным дампом
Что произошло? Почему такие ужасные последствия у, казалось бы, маленьких изменений? Причина кроется в способе вызова методов — по индексу в списке внутри класса объекта. Мы вызываем метод B.set(int), допустим он имеет индекс 1, но вызываем то мы его на объекте типа A! У него другой список, и под индексом 1, может быть, идёт конструктор. Это несоответствие и вызывает ошибку.
Но почему такого не происходит при доступе к полям? Ответ прост — они всегда имеют постоянное смещение и его нет нужды вычислять каждый раз по индексу поля в классе, а значит, для оптимизации всегда идёт прямой доступ по заранее просчитанному смещению.
В будущем мы периодически будем обращаться к arrayCast для достижения своих целей. На этом вводная часть подходит к концу.
Весь исходный код можно найти здесь.
- java
- android
- unsafe
- reinterpret_cast
- ненормальное программирование
Unsafe java что это
Вы используете устаревший браузер. Этот и другие сайты могут отображаться в нём некорректно.
Необходимо обновить браузер или попробовать использовать другой.
Unsafe Java I — Небезопасная жаба
Дата публикации 20 май 2006
Unsafe Java I — Небезопасная жаба — Архив WASM.RU
- Класс sun.misc.Unsafe
- Структуры виртуальной машины
- Особенности версии 1.5
- Применение на практике
- Бесконечный final
public static Unsafe getUnsafe()
Class class1 = Reflection.getCallerClass(2);
if(class1.getClassLoader() != null)
throw new SecurityException(«Unsafe»);
return theUnsafe;
К счастью существует еще внутренняя переменная theUnsafe, до которой мы можем добраться с помощью Reflection. Всю черновую работу соберем в один класс (назовем его UnsafeUtil), который будем расширять по мере надобности.
public class UnsafeUtil < public static Unsafe unsafe; private static long fieldOffset; private static UnsafeUtil instance = new UnsafeUtil(); private Object obj; Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); unsafe = (Unsafe)f.get(null); fieldOffset = unsafe.objectFieldOffset(UnsafeUtil.class.getDeclaredField("obj")); > catch (Exception ex) < throw new RuntimeException(e);
Конечно можно просто внести UnsafeUtil в список загружаемых Bootloader’ом классов (указав путь в ключе -Xbootclasspath/a) и вызывать getUnsafe() в соответствии с замыслом Sun. Беда в том, что тогда все использующие UnsafeUtil классы также должны быть прописаны в bootclasspath’е (см. главу «5.3 Creation and Loading» в VM spec). Правда например package java.nio как-то ухитряется обходить это ограничение, но как именно пока не очень понятно. К тому же этот способ выходит за рамки «чистого» кода, так как требует дополнительных стартовых опций для виртуальной машины. Так что не будем мудрствовать и ограничимся чтением theUnsafe.
В первую очередь нам понадобятся естественно операции референцирования и дереференцирования, ObjectToAddress и AddressToObject соответственно.
public static long ObjectToAddress (Object o)< instance.obj = o; return unsafe.getLong(instance, fieldOffset); public static Object AddressToObject (long address)< unsafe.putLong(instance, fieldOffset, address); return instance.obj;
С ними мы уже достаточно хорошо вооружены в техническом плане, не хватает только информации по внутреннему устройству Явы. Ее мы найдем в следующем разделе.
Очень похожую реализацию кстати сделал Don Schwarz (http://don.schwarz.name/index.php?p=30). Это одно из очень немногих мест, где можно найти хоть какие-то примеры работы с классом Unsafe. К сожалению Don в свое время не оценил потенциал низкоуровнего программирования в Яве и остановился, сделав всего пару робких шагов. Мы же пойдем дальше.
2. Структуры виртуальной машины
Теперь посмотрим, в каком виде виртуальная машина (версии 1.4) хранит данные в памяти. Поскольку Ява работает с классами и их инстанциями, то ими и займемся.
Инстанция:
instance_struct <
0 magic // всегда равен 1
4 class // указатель на структуру класса, class_struct*
8 . // Дальше идут подряд переменные инстанции(то есть все которые не static),
12 . // порядок пока не очень понятен, судя по всему в порядке объявления и
16 . // обьекты перед примитивными типами
Переменные типа double и long занимают 64 бита, остальные по 32. В памяти инстанции выравниваются по 64-битной границе, дополняются при необходимости нулями. То есть по сути мы имеем обыкновенную сишную структуру плюс указатель на ее описание.
Класс:
class_struct <
0 magic // всегда равен 1
4 class // class_struct*, указатель на структуру класса более высокого уровня, зачем нужен — непонятно
8 . // значение неизвестно
12 super_count // количество уровней наследования: 0x18 — один(наследует от Object), 0х1c — два и т.д. до восьми(0х34), потом 0х10. У интерфейсов тоже 0х10.
16 interface // class_struct*, указатель на какой-либо из интерфейсов класса (на какой именно непонятно), часто просто 0
20 interface_list // указатель на массив с элементами типа class_struct*, все интерфейсы класса
36 . // указатели на структуры восьми высших суперклассов начиная от Object и кончая this (если поместится)
56 size // размер инстанции класса в DWORD’ах
60 this_class // instance_struct*, указатель на инстанцию java.lang.Class соответствующую данному классу
64 access_flags // доступ к классу как описано в VM spec ( 0х1 — public, 0х10 — final и т.д.)
Здесть я привел только те куски, которые мы будем использовать в дальнейшем и в которых я более или менее уверен. На самом деле class_struct значительно длиннее и содержит кроме того указатели на функции класса, статические переменные и все остальное, что может понадобится виртуальной машине. Все эти структуры по понятным причинам нигде не документированы и разбираться надо вручную — хоть и несложно, но достаточно трудоемко. Если у кого-то есть желание помочь, буду только рад.
3. Особенности версии 1.5
С переходом на последнюю (на момент написания) версию 1.5.0_06 внутренние структуры виртуальной машины претерпели некоторые изменения. К счастью небольшие: изменился в основном порядок полей, значения остались в большинстве прежними. Структура класса выглядит теперь следующим образом:
class_struct_1_5 < 0 magic // всегда равен 1
4 class // class_struct*, указатель на структуру класса более высокого уровня, зачем нужен — непонятно
8 . // значение неизвестно
12 size // размер инстанции класса в DWORD’ах
16 super_count // количество уровней наследования: 0x20 — один(наследует от Object), 0х24 — два и т.д. до восьми(0х38), потом 0х14. У интерфейсов тоже 0х14.
20 interface // class_struct*, указатель на какой-либо из интерфейсов класса (на какой именно непонятно), часто просто 0
24 interface_list // указатель на массив с элементами типа class_struct*, все интерфейсы класса
40 . // указатели на структуры восьми высших суперклассов начиная от Object и кончая this (если поместится)
60 this_class // instance_struct*, указатель на инстанцию java.lang.Class соответствующую данному классу
Путеводитель по sun.misc.Unsafe
В этой статье мы рассмотрим увлекательный класс, предоставляемый 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, поэтому его должно быть легко импортировать и запускать как есть.