Как могут работать атомарные операции в Java, и есть ли текущая альтернатива в OpenJDK / Hotspot, на которую она может быть переведена.
Обратная связь
В моей предыдущей статье о том, как сделать операции над летучими полями атомарными. Несколько раз отмечалось, что «исправление» предыдущего поведения вряд ли будет продолжаться независимо от благих намерений.
Альтернативой этому является добавление анатомии @atomic. Это имеет преимущество только в применении к новому коду и не рискует сломать старый код.
Примечание. Использование имени в нижнем регистре является преднамеренным, поскольку оно * не * соответствует действующим правилам кодирования.
Атомные операции
Любое поле, указанное с помощью @atomic, сделает все выражение атомарным. Переменные, которые являются энергонезависимыми и неатомарными, могут быть прочитаны в начале или установлены после завершения выражения. Само выражение может потребовать блокировки на некоторых платформах, операций CAS или TSX в зависимости от технологии ЦП.
Если поля только для чтения или написано только одно, это будет то же самое, что и volatile.
Булево атомное
В настоящее время AtomicBoolean использует 4 байта плюс заголовок объекта с возможным заполнением (а также ссылкой). Если поле было встроено, оно могло бы выглядеть следующим образом
@atomic boolean flag; // toggle the flag. this.flag = !this.flag;
Но как это будет работать? Не все платформы поддерживают 1-байтовые атомарные операции, например, Unsafe имеет 1-байтовые операции CAS. Это можно сделать с помощью маскировки.
// possible replacement. while(true) { int num = Unsafe.getUnsafe().getVolatileInt(this, FLAG_OFFSET & ~3); // word align the access. int value ^= 1 << ~(0xFF << (FLAG_OFFSET & 3) * 8) ; if (Unsafe.getUnsafe().compareAndSwapInt(this, FLAG_OFFSET & ~3, num, value)) break; }
Атомный двухместный
Тип, который не поддерживается, это AtomicDouble, но это вариант AtomicLong. Рассмотрим этот пример.
@atomic double a = 1; volatile double b = 2; a += b;
Как это может быть реализовано сегодня?
while(true) { double _b = Unsafe.getUnsafe().getVolatileDouble(this, B_OFFSET); double _a = Unsafe.getUnsafe().getVolatileDouble(this, A_OFFSET); long aAsLong = Double.doubleToRawLongBits(_a); double _sum = _a + _b; long sumAsLong = Double.doubleToRawLongBits(_a); if (Unsafe.getUnsafe().compareAndSwapLong(this, A_OFFSET, aAsLong, sumAsLong)) break; }
Два атомных поля
Используя Intel TSX, вы можете обернуть аппаратную транзакцию вокруг нескольких полей, но что, если у вас нет TSX, все равно это можно сделать, не прибегая к блокировке.
@atomic int a = 1, b = 2; a += b * (b % 2 == 0 ? 2 : 1);
Это все еще можно сделать с помощью CAS, если поля вместе. Запланирована операция CAS2, чтобы иметь возможность проверить два 64-битных значения. Пока этот пример будет использовать два 4-байтовых значения.
assert A_OFFSET + 4 == B_OFFSET; while(true) { long _ab = Unsafe.getUnsafe().getVolatileLong(this, A_OFFSET); int _a = getLowerInt(_ab); int _b = getHigherInt(_ab); int _sum = _a + _b * (_b % 2 == 0 ? 2 : 1); int _sum_ab = setLowerIntFor(_ab, _sum); if (Unsafe.getUnsafe().compareAndSwapLong(this, A_OFFSET, _ab, _sum_ab)) break; }
Примечание. Эта операция может обрабатывать изменение либо a, либо b, либо обоих элементов атомарным способом.
AtomicReferences
Операции общего случая использования с неизменяемыми объектами, такими как BigDecimal.
@atomic BigDecimal a; BigDecimal b; a = a.add(b);
может быть реализован таким образом в системах с CompressedOops или 32-битных JVM.
BigDecimal _b = this.b; while(true) { BigDecimal _a = (BigDecimal) Unsafe.getUnsafe().getVolatileObject(this, A_OFFSET); BigDecimal _sum = _a.add(_b); if (Unsafe.getUnsafe().compareAndSwapLong(this, A_OFFSET, _a, _sum)) break; }
Более сложные примеры
Всегда будут примеры, которые слишком сложны для вашей платформы. Они могут подойти для Системы с системами, поддерживаемыми TSX или HotSpot, однако вам нужно отступить.
@atomic long a, b, c, d; a = (b = (c = d + 4) + 5 ) + 6;
В настоящее время это не поддерживается, поскольку в одном выражении задано несколько длинных значений. Однако отступление может заключаться в использовании существующей блокировки.
synchronized(this) { a = (b = (c = d + 4) + 5 ) + 6; }
Вывод
Добавив аннотацию, мы могли бы добавить элементарные операции к обычным полям без необходимости изменять синтаксис. Это было бы естественным расширением языка без нарушения обратной сопоставимости.