⚡️ Микро-оптимизация: Прощай, AtomicInteger, привет, VarHandle!
👋 Сегодня поговорим о том, как можно немного ускорить ваш код, работающий с атомарными операциями, и
сделать его более "современным" с помощью VarHandle, который появился в Java 9.
🧐 Проблема с AtomicInteger
AtomicInteger - это классический способ обеспечить атомарные операции (например, инкремент) над целым
числом без блокировок, используя механизм Compare-And-Swap (CAS).
Однако, у него есть небольшой недостаток: он добавляет слой косвенности. Методы типа getAndIncrement()
в AtomicInteger обычно вызывают внутренние статические методы из класса sun.misc.Unsafe
(или его аналогов в более новых версиях), передавая в них ссылку на объект, смещение поля и новое значение.
// Примерно так это выглядит внутри AtomicInteger
// В реальном коде это, конечно, оптимизировано, но суть та же.
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
✨ Встречайте VarHandle!
VarHandle - это новый, более гибкий и, что самое главное, более производительный низкоуровневый API для
работы с полями переменных (как экземпляров, так и статических) с заданными барьерами памяти и атомарностью.
Главное преимущество? JIT-компилятор (C2) лучше оптимизирует доступ через VarHandle, чем через старые обертки
типа AtomicInteger или прямые вызовы Unsafe.
🛠 Как использовать VarHandle вместо AtomicInteger
Вместо того, чтобы хранить значение в отдельном объекте AtomicInteger, мы просто объявляем поле volatile в нашем
классе и получаем VarHandle для этого поля.
1. Объявляем поле:
public class Counter {
private volatile int count = 0; // Поле должно быть volatile
// ...
}
2. Создаем VarHandle:
VarHandle нужно инициализировать один раз (обычно в статическом блоке) для доступа к полю.
private static final VarHandle COUNT_HANDLE;
static {
try {
// Получаем VarHandle для поля 'count' класса 'Counter' с типом int
COUNT_HANDLE = MethodHandles.lookup().findVarHandle(
Counter.class,
"count",
int.class
);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
}
3. Выполняем атомарную операцию:
Метод getAndAdd VarHandle работает точно так же, как getAndAdd в AtomicInteger.
public int increment() {
// В отличие от AtomicInteger, где первый аргумент неявен,
// VarHandle требует первым аргументом *объект*,
// к полю которого мы обращаемся.
return (int) COUNT_HANDLE.getAndAdd(this, 1);
}
🚀 Результат микро-оптимизации
В бенчмарках (например, с использованием JMH) можно увидеть, что операции getAndAdd() через
VarHandle могут быть незначительно быстрее (зачастую на 5-10%), чем те же операции через AtomicInteger,
особенно под высокой нагрузкой, благодаря более эффективной генерации кода JIT-компилятором.
🛑 Важно: Это микро-оптимизация. В большинстве приложений вы не заметите разницы. Но если вы
пишете критически важные фреймворки, высоконагруженные коллекции или библиотеки, где каждая наносекунда на счету,
переход на VarHandle может быть оправдан.
VarHandle - это не только способ микро-оптимизации, но и стандартный, гибкий API для атомарного доступа, который
заменил устаревший и менее безопасный Unsafe. Для нового кода, где требуется низкоуровневый атомарный доступ,
стоит отдавать предпочтение именно ему.
👉 @BookJava