Анализ принципа Java CAS

Java JVM

В параллелизме Java наш первоначальный контакт должен бытьsynchronizedключевое слово, ноsynchronizedОн принадлежит к тяжеловесному замке, что приведет к тому, что проблемы с производительностью много раз.volatileтоже хороший выбор, ноvolatileАтомарность не гарантируется и может использоваться только в определенных ситуациях.

рисунокsynchronizedЭтот эксклюзивный замок принадлежитпессимистический замок, предполагается, что должен быть конфликт, тогда блокировка оказывается полезной, кроме того, естьоптимистическая блокировка, смысл оптимистической блокировки заключается в том, чтобы предположить, что конфликта нет, тогда я могу просто выполнить операцию. Если есть конфликт, то я буду повторять попытку до тех пор, пока она не увенчается успехом. Наиболее распространенная оптимистическая блокировкаCAS.

Когда мы прочитали исходный код классов в пакете Concurrent, мы обнаружили, что либоAQS внутри ReenterLock также является атомарным классом в начале различных атомарных, которые применяются внутрьCAS, чаще всего это то, с чем мы сталкиваемся в параллельном программированииi++Эта ситуация. Традиционный метод определенно добавляет методsynchronizedКлючевые слова:

public class Test {

    public volatile int i;

    public synchronized void add() {
        i++;
    }
}

Но этот метод может быть немного хуже по производительности, мы также можем использоватьAtomicInteger, можно гарантироватьiатомный++.

public class Test {

    public AtomicInteger i;

    public void add() {
        i.getAndIncrement();
    }
}

ПосмотримgetAndIncrementвнутренний:

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

идти глубжеgetAndAddInt():

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

Здесь мы видимcompareAndSwapIntэта функция, она такжеCASПроисхождение аббревиатуры. Итак, внимательно проанализируйте, что делает эта функция?

Сначала мы находимcompareAndSwapIntфронтthis, то к какому классу он относится, давайте посмотрим на предыдущий шагgetAndAddInt, спередиunsafe. Здесь мы входимUnsafeсвоего рода. Прямо здесьUnsafeкласс объяснить. комбинироватьAtomicIntegerЗначение:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;
    
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    
    private volatile int value;
    ...

существуетAtomicIntegerВ части определения данных мы видим, что фактическое сохраненное значение помещается вvalue, кроме того, мы также получаемunsafeэкземпляр и определяетvalueOffset. увидеть сноваstaticблок, знает каждый, кто разбирается в процессе загрузки класса,staticЗагрузка блока происходит, когда класс загружается и инициализируется первым, в это время мы вызываемunsafeизobjectFieldOffsetотAtomicфайл классаvalueсмещение, затемvalueOffsetэто вообще рекордvalueсмещения.

Вернитесь к функции вышеgetAndAddInt, мы видимvar5что получается при вызовеunsafeизgetIntVolatile(var1, var2), это нативный метод, конкретную реализацию можно увидеть в исходниках JDK, на самом деле он заключается в полученииvar1середина,var2Значение по смещению.var1этоAtomicInteger,var2то, что мы упоминали ранееvalueOffset, так что мы попадаем из памяти в настоящееvalueOffsetстоимость в.

Теперь наступает момент,compareAndSwapInt(var1, var2, var5, var5 + var4)На самом деле, замените его наcompareAndSwapInt(obj, offset, expect, update)Понятно, что еслиobjвнутриvalueиexpectравным, это доказывает, что ни один другой поток не изменил эту переменную, а затем обновите ее доupdate, если этот шагCASЕсли это не сработает, то используйте метод вращения, чтобы продолжитьCASОперация, вынесите на первый взгляд, это тоже два шага, на самом деле вJNIс помощьюCPUинструкция завершена. Так что это все еще атомарная операция.

Основной принцип CAS

Базовое использование CASJNIВызовите реализацию кода C, если у вас естьHotspotисходный код, затем вUnsafe.cppЕго реализацию можно найти в:

static JNINativeMethod methods_15[] = {
    //省略一堆代码...
    {CC"compareAndSwapInt",  CC"("OBJ"J""I""I"")Z",      FN_PTR(Unsafe_CompareAndSwapInt)},
    {CC"compareAndSwapLong", CC"("OBJ"J""J""J"")Z",      FN_PTR(Unsafe_CompareAndSwapLong)},
    //省略一堆代码...
};

Мы видим, что реализация compareAndSwapInt находится вUnsafe_CompareAndSwapIntвнутри, дальше вUnsafe_CompareAndSwapInt:

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

p — извлеченный объект, addr — адрес со смещением в p, и, наконец, вызываетсяAtomic::cmpxchg(x, addr, e), где параметр x — это значение, которое нужно обновить, а параметр e — это значение исходной памяти. По коду видно, что cmpxchg имеет реализации на разных платформах, здесь я выбираю анализ исходного кода под платформу 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;
}

Это небольшая подборка,__asm__Описание сборки ASM,__volatile__Отключить оптимизацию компилятора

// Adding a lock prefix to an instruction on MP machine
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

os::is_MPОпределите, является ли текущая система многоядерной, и если да, заблокируйте шину, чтобы другие процессоры на том же кристалле не могли временно получить доступ к памяти через шину, обеспечив атомарность инструкции в многопроцессорной среде.

Прежде чем формально интерпретировать эту сборку, давайте разберемся с основным форматом встроенной сборки:

asm ( assembler template
    : output operands                  /* optional */
    : input operands                   /* optional */
    : list of clobbered registers      /* optional */
    );
  • templateэтоcmpxchgl %1,(%3)Представляет шаблон сборки

  • output operandsпредставляет выходной операнд,=aСоответствует регистру eax

  • input operandпредставляет входной параметр,%1этоexchange_value, %3даdest, %4этоmp,rпредставляет любой регистр,aвсе ещеeaxрегистр

  • list of clobbered registersпросто дополнительные параметры,ccозначает компиляторcmpxchglВыполнение повлияет на регистр флагов,memoryСкажите компилятору перечитать последнее значение переменной из памяти, что реализованоvolatileощущение.

Тогда выражение действительноcmpxchgl exchange_value ,dest, мы найдем%2этоcompare_valueЕсли он не используется, он будет проанализирован здесьcmpxchglсемантика.cmpxchglконецlУказывает, что длина операнда4, уже известное выше.cmpxchglбудет сравнивать по умолчаниюeaxЗначение регистраcompare_valueиexchange_valueзначение ,Если они равны, положитьdestЗначение, присвоенноеexchange_value, в противном случаеexchange_valueназначить вeax. Конкретные инструкции по сборке см. в руководстве Intel.CMPXCHG

В конечном счете, JDK проходит через ЦП.cmpxchglРеализована поддержка директивAtomicIntegerизCASАтомарность операций.

Проблемы с CAS

  1. АВА-проблема

CAS должен проверить, изменилось ли значение при работе со значением, и обновить его, если изменений нет, но если значение изначально было A, стало B, а затем стало A, то оно будет найдено при использовании CAS для проверки. Значение не изменилось, но оно действительно изменилось. Это проблема ABA CAS. Распространенным решением является использование номеров версий. Добавьте номер версии перед переменной и увеличивайте номер версии на единицу каждый раз, когда переменная обновляется, затемA-B-Aстанет1A-2B-3A. В настоящее время класс предоставляется в атомарном пакете JDK.AtomicStampedReferenceдля решения проблемы АБА. Роль метода compareAndSet этого класса состоит в том, чтобы сначала проверить, равна ли текущая ссылка ожидаемой ссылке, и равен ли текущий флаг ожидаемому флагу, и, если все равны, атомарно установить ссылку и значение флаг на заданное обновленное значение.

  1. Длительное время цикла и высокие накладные расходы

Выше мы сказали, что если CAS будет неудачным, то он будет крутиться на месте, а если крутится долго, то принесет очень большие накладные расходы на выполнение CPU.