Введение
В Java многие классы инструментов используют CAS (Compare And Set) для повышения эффективности параллелизма и точности данных.
- Многие классы, такие как AtomicInteger, в concurrent и concurrent.atomic
- ReentrantLock, WriteLock и т. д. в пакете concurrent.locks
- разное
Для большинства людей наиболее распространенным является использование AtomicXXX, а при использовании подклассов, связанных с блокировкой, мы знаем, что их основа использует CAS, и мы знаем, что CAS должен передавать ожидаемое значение (ожидание) и значение, которое необходимо update (обновление), если требования соблюдены, выполняется обновление, в противном случае при выполнении не удается добиться атомарности данных.
Мы знаем, что CAS каким-то образом гарантирует атомарность данных внизу.
- Нет необходимости делать много накладных расходов, таких как синхронная блокировка, приостановка и пробуждение потока.
- Доверить работу по обеспечению атомарности данных базовому оборудованию производительность намного выше, чем выполнять синхронную блокировку, приостановку, пробуждение и другие операции, поэтому его параллелизм лучше.
- Последующие операции могут быть определены в соответствии со статусом, возвращенным CAS для достижения согласованности данных, например, ошибка приращения, затем цикл значений до успеха (будет обсуждаться ниже) и т. д.
Сначала посмотрите на неправильный приращение ()
private int value = 0;
public static void main(String[] args) {
Test test = new Test();
test.increment();
System.out.println("期待值:" + 100 * 100 + ",最终结果值:" + test.value);
}
private void increment() {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (int j = 0; j < 100; j++) {
value++;
}
}).start();
}
}
вывод:期待值:10000,最终结果值:9900
Можно обнаружить, что значение выходного результата неверно, потому чтоvalue++
это не атомарная операция, она будетvalue++
Разделить на 3 шагаload、add、store
, многопоточный параллелизм может не сохранить хранилище после того, как предыдущий поток добавил, а следующий поток снова выполняет загрузку. Результат этого повторения может быть меньше конечного значения.
конечно добавить сюда
volatile int value
Это также бесполезно, потому что сама 32-битная операция int является атомарной, и volatile не может сделать эти 3 операции атомарными.Он может только запретить переупорядочивание определенной инструкции, чтобы обеспечить видимость соответствующей памяти, если онаlong 等 64 位操作类型的可以加上 volatile
, потому что операция записи на 32-битной машине может быть назначена разным шинным транзакциям (ее можно представить как двухэтапную операцию, первая операция — первые 32 бита, а вторая операция — последние 32 бита), и шинная транзакция. Выполнение определяется шинным арбитражем, и порядок его выполнения не может быть гарантирован (эквивалентно предыдущему добавлению 32 бит, он может переключаться на другие места для выполнения, например, непосредственное чтение, тогда чтение данных будет только считано а пишется половина стоимости)
Используйте CAS, чтобы убедиться, что increment() правильный
Мы знаем, что операции с CAS в основном инкапсулированы в пакете Unsafe, но поскольку Unsafe не позволяет нам использовать его извне, он считает это небезопасной операцией, например, если она используется напрямую.Unsafe unsafe = Unsafe.getUnsafe();
броситException in thread "main" java.lang.SecurityException: Unsafe
.
Мы посмотрели исходный код, он оказался, потому что он был проверен
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
Так что мы можем вызвать его через рефлексию (конечно, это не рекомендуется в реальной работе, здесь для удобства демонстрации)
public class Test {
// value 的内存地址,便于直接找到 value
private static long valueOffset = 0;
{
try {
// 这个内存地址是和 value 这个成员变量的值绑定在一起的
valueOffset = getUnsafe().objectFieldOffset
(Test.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private int value;
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Test test = new Test();
test.increment();
}
private void increment() throws NoSuchFieldException, IllegalAccessException {
Unsafe unsafe = getUnsafe();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
unsafe.getAndAddInt(this, valueOffset, 1);
}
}).start();
}
System.out.println("需要得到的结果为: " + 100 * 1000);
System.out.println("实际得到的结果为: " + value);
}
// 反射获取 Unsafe
private Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
}
}
Теперь мы можем видеть из вывода, что результат правильный
Основной принцип реализации CAS
Продолжаем исследовать, вызывается getAndAddIntunsafe.compareAndSwapInt(Object obj, long valueOffset, int expect, int update)
Как этот метод реализован в Hotspot, мы выяснили, что вызов нативныйunsafe.compareAndSwapInt(Object obj, long valueOffset, int expect, int update)
, смотрим исходный код Hotspot и обнаруживаем, что такой кусок кода определен в unsafe.cpp
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe,
jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
Отсюда мы видим, что он используетсяAtomic::cmpxchg(x, addr, e)
Эта операция выполнена, и на разных базовых аппаратных средствах будут разные коды. Точка доступа помогает нам скрыть детали. Этот метод реализации имеет разные методы реализации в solaris, windows, linux_x86 и т.д. Мы используем в качестве примера наш самый распространенный сервер linux_x86, код его реализации выглядит следующим образом
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest,
jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
Из приведенного выше кода видно, что
- Hotspot напрямую вызывает базовую сборку для реализации соответствующей функции.
-
__asm__
Указывает, что продолжение является фрагментом ассемблерного кода. -
volatile
Между volatile здесь и Java есть некоторые различия, которые используются здесь, чтобы указать компилятору больше не выполнять оптимизацию сборки для этого кода. -
LOCK_IF_MP
Это означает, что если операционная система многоядерная, ее необходимо заблокировать, чтобы обеспечить ее атомарность. -
cmpxchgl
это сравнение и обмен в сборке
Из этого видно, что нижний слой CAS также использует блокировки для обеспечения своей атомарности. В ранней реализации Intel шина была заблокирована напрямую, так что другие процессоры, не получившие права доступа к транзакциям шины, не могли выполнять последующие операции, и производительность сильно снижалась.
Последующая Intel оптимизировала и модернизировала его.В процессорах x86 необходимо блокировать только определенные адреса памяти, тогда другие процессоры могут продолжать использовать шину для доступа к данным памяти, но если другим шинам также требуется доступ к заблокированной памяти, он будет только блокировать данные адреса оперативной памяти, что значительно повышает производительность.
Но рассмотрите следующие вопросы
- Количество параллелизма очень велико, что может привести к постоянной конкуренции за значение, что может привести к тому, что многие потоки будут находиться в постоянном состоянии цикла и не смогут обновлять данные, что приведет к высокому потреблению ресурсов ЦП.
- Проблема ABA, например, предыдущий поток добавил определенное значение и изменил определенное значение, а затем следующий поток думал, что данные не изменились, но на самом деле они были изменены
Оптимизация JAVA8 для CAS
Конечно, проблему ABA можно контролировать, увеличивая номер версии.Каждый раз, когда используется номер версии + 1, значение номера версии изменяется и значение изменяется один раз.В Java класс AtomicStampedReference предоставляет решение к этой проблеме.
Для первой проблемы в Java 8 есть соответствующие оптимизации. В Java 8 предусмотрены некоторые новые классы инструментов для решения этой проблемы, а именно:
Давайте выберем один и посмотрим, остальные похожи
Его принцип в основном принимаетМеханизм сегментации CAS и механизм автоматической миграции сегментации, сначала выполняется операция CAS над базой, а последующих параллельных потоков слишком много, затем это большое количество потоков распределяется по массиву ячеек, и потоки каждого массива выполняют операцию накопления отдельно, и, наконец, сливают Результаты.
Изображение из[Базовая консолидация] Оптимизация CAS в Java 8
Суммировать
Можно видеть, что если вы можете разумно использовать CAS для работы или объединения двух, то одновременная производительность может быть улучшена на порядок по сравнению с прямой приостановкой или пробуждением потоков.
- Для таких вещей, как ReentrantLock, синхронная блокировка + CAS используется для достижения высокопроизводительных блокировок, таких как tryAcuqire() в ReentrantLock. Если соответствующую блокировку нельзя получить с помощью CAS, она будет помещена в очередь блокировки. , ожидая последующих пробуждений
- Например, если спин-блокировка не может получить блокировку через CAS указанное количество раз, она зависнет в очереди блокировки и будет ждать пробуждения.
- Например, при использовании AtomicInteger для автоинкремента он будет постоянно опрашивать значение для оценки и обновления до тех пор, пока операция не будет успешной.
- Если вы используете обработку CAS с опросом без встроенной блокировки приостановки и пробуждения, ее преимущество заключается в том, что она может быстро реагировать на запросы пользователей и сокращать потребление ресурсов, поскольку приостановка и пробуждение потока включают в себя вызовы пользовательского режима в режиме ядра и «моментальные снимки» потока. хранилище медленное и высокое для отклика и потребления ресурсов, но нам также необходимо учитывать накладные расходы на опрос ЦП, чтобы в определенной степени их можно было использовать вместе.
- Поэтому понимание CAS по-прежнему очень важно.
Ссылаться на:CAS в JAVA