Проблемы и решения ABA, вызванные CAS

Java

Параллелизм Java — неблокирующая синхронизация

Введение в проблему CAS

При проблемах параллелизма первое, что приходит на ум, несомненно, синхронизация с взаимоисключающими блокировками, но блокировка потоков и пробуждение приносят большие проблемы с производительностью. всегда есть такая конкуренция.

Что такое КАС

CAS, сравнить и обменять (Compare-and-Swap, CAS), если ожидаемое значение совпадает со значением в основной памяти, то заменить значение, которое нужно обновить, также известное как оптимистическая блокировка.

Если поток A копирует переменную A из основной памяти в 1 и изменяет копию A на 10 в своем собственном потоке, когда поток A готовится обновить эту переменную в основную память, если значение основной памяти A не изменяется (ожидаемое значение), или 1, то поток A успешно обновляет значение A в основной памяти. Но если значение основной памяти A было изменено другими потоками первым и не равно 1, то поток A продолжает повторять попытки до тех пор, пока не добьется успеха (spin).

откуда КАС

КАС принадлежитJ.U.Cпакет, называемыйUnsafeМетод в классе, который представляет собой атомарную операцию с аппаратной поддержкой, которую нельзя прервать или остановить без синхронизации взаимного исключения.

Возьмите метод getAndAddInt в AtomicInteger в качестве примера, U — это класс Unsafe.

/**
  * @param expectedValue 期望值
  * @param newValue 新值
  * @return 比价更新是否成功.
  */
public final int incrementAndGet() {
    return U.getAndAddInt(this, VALUE, 1) + 1;
}

Глядя дальше, получите предыдущее значение v через getIntVolatile(o, offset), вызвавweakCompareAndSetInt()Чтобы выполнить сравнение CAS, если значение в адресе памяти этого поля равно v, тогда обновите переменную, адрес памяти которой равен o+offset, на v + delta. Метод getAndAddInt() выполняется в цикле,Практика конфликта состоит в том, чтобы продолжать повторять попытки.

/**
     * @param o 更新字段/元素的对象/数组
     * @param offset 字段/元素偏移量
     * @param delta 要添加的值,步长
     * @return 以前的值
     * @since 1.8
     */
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}

Запустите пример кода

Потоки t1, t2 одновременно изменяют значение переменной в основной памяти, искусственно делая B быстрее, чем A

public class TestCAS {
    // 主内存atomicInteger初始值为1
    public static AtomicInteger atomicInteger = new AtomicInteger(1);

    public static void main(String[] args) {
        // A线程计划将值改为10,先休眠2s,再比较交换
        new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("当前线程: "+Thread.currentThread().getName()+"比较交换结果:"
                    +atomicInteger.compareAndSet(1, 10)+" 现在值为:"+atomicInteger.get());
        },"t1").start();

        // B线程计划将值改为10,不休眠
        new Thread(() -> {
            System.out.println("当前线程: "+Thread.currentThread().getName()+"比较交换结果:"
                    +atomicInteger.compareAndSet(1, 20)+" 现在值为:"+atomicInteger.get());
        },"t2").start();
    }
}

консоль

当前线程: t2比较交换结果:true 现在值为:20
当前线程: t1比较交换结果:false 现在值为:20

Таким образом, нет необходимости добавлять блокировки синхронизации, и решаются проблемы, связанные с одновременным изменением переменных.

Если ваш хороший друг одолжил у вас 10 юаней и дал вам 10 юаней на следующий день, если ваш друг только что купил пакет закусок, вам может быть все равно, как он использовал джекпот в 10 юаней, возможно, вы торопитесь ...

Введение в проблему ABA

В предыдущем коде есть проблема. Например: потоки t1, t2 копируются в переменную atomicInteger=1, если поток B имеет более высокий приоритет или повезло, первый раз, t2 сначала меняет atomicInteger на 20 и успешно записывает его в основную память, а затем t2 копируется в atomicInteger= 20, снова изменяет копию на 1 и успешно записывает обратно в основную память. В третий раз t1 получает значение atomicInteger в основной памяти. Но это значение было дважды изменено t2, не будет ли проблем?

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

Если переменная была сначала прочитана со значением A, ее значение было изменено на B, а затем изменено обратно на A, операция CAS ошибочно приняла бы это за тот факт, что оно никогда не изменялось.

АВА-решение

  1. Блокировка синхронизации взаимного исключения синхронизирована

  2. Если проект заботит только правильность значений, то проблема ABA не влияет на корректность параллелизма программ.

  3. Пакет J.U.C предоставляет атомарный ссылочный класс с метками времени.AtomicStampedReferenceрешить проблему путемверсия управляющей переменнойдля обеспечения правильности CAS.

Пример кода AtomicStampedReference

public class SolveCAS {
    // 主内存共享变量,初始值为1,版本号为1
    private static AtomicStampedReference<Integer> atomicStampedReference = new
            AtomicStampedReference<>(1, 1);


    public static void main(String[] args) {
        // t1,期望将1改为10
        new Thread(() -> {
            // 第一次拿到的时间戳
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+" 第1次时间戳:"+stamp+" 值为:"+atomicStampedReference.getReference());
            // 休眠5s,确保t2执行完ABA操作
            try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
            // t2将时间戳改为了3,cas失败
            boolean b = atomicStampedReference.compareAndSet(1, 10, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName()+" CAS是否成功:"+b);
            System.out.println(Thread.currentThread().getName()+" 当前最新时间戳:"+atomicStampedReference.getStamp()+" 最新值为:"+atomicStampedReference.getReference());
        },"t1").start();

        // t2进行ABA操作
        new Thread(() -> {
            // 第一次拿到的时间戳
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+" 第1次时间戳:"+stamp+" 值为:"+atomicStampedReference.getReference());
            // 休眠,修改前确保t1也拿到同样的副本,初始值为1
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            // 将副本改为20,再写入,紧接着又改为1,写入,每次提升一个时间戳,中间t1没介入
            atomicStampedReference.compareAndSet(1, 20, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName()+" 第2次时间戳:"+atomicStampedReference.getStamp()+" 值为:"+atomicStampedReference.getReference());
            atomicStampedReference.compareAndSet(20, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName()+" 第3次时间戳:"+atomicStampedReference.getStamp()+" 值为:"+atomicStampedReference.getReference());

        },"t2").start();
    }
}

консоль

t1 第1次时间戳:1 值为:1
t2 第1次时间戳:1 值为:1
t2 第2次时间戳:2 值为:20
t2 第3次时间戳:3 值为:1
t1 CAS是否成功:false
t1 当前最新时间戳:3 最新值为:1