Как могут работать атомарные операции в 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;
}
Вывод
Добавив аннотацию, мы могли бы добавить элементарные операции к обычным полям без необходимости изменять синтаксис. Это было бы естественным расширением языка без нарушения обратной сопоставимости.