Атомарный атомарный класс и анализ исходного кода, связанный с CAS

Java

1. Атомные классы J.U.C.

1.1 Введение в атомарные классы

  • Атомарный класс, неотделимый от манипулирования данными объекта. атомный
  • Функция: Функция атомарного класса аналогична функции блокировки, чтобы обеспечить безопасность потока в случае параллелизма, но атомарный класс имеет определенные преимущества по сравнению с блокировкой:
    1. Более тонкая детализация: атомарные классы снижают конкуренцию до уровня переменных
    2. Более высокая эффективность: обычно выше, но менее эффективна в условиях жесткой конкуренции.
  • Большинство атомарных классов в J.U.C реализованы CAS (в конце будет анализ исходного кода)

1.2 Atomic*атомарный класс базового типа

  • Возьмите AtomicInteger в качестве примера
  1. Общий метод
    • int get()получить текущее значение
    • int getAndSet(int)Получить текущее значение и установить новое значение
    • int getAndIncrement()Получить текущее значение и увеличить его
    • int incrementAndGet()Получить значение после автоинкремента (по сравнению с тем же методом get получает значение до автоинкремента перед get, get получает значение после автоинкремента после)
    • int getAndDecrement()Получить текущее значение и уменьшить его
    • int getAndAdd(int)Получить текущее значение и добавить значение
    • boolean compareAndSet(int expect, int update)Определить, соответствует ли текущее значение ожидаемому значениюexpect, если совпадает, установить значение обновленияupdate.
  2. Пример использования
/**
 * 使用AtomicInteger 对比 非原子类,演示线程安全问题
 *
 * @author yiren
 */
public class AtomicIntegerExample01 {
    private static AtomicInteger atomicInteger = new AtomicInteger();
    private static volatile Integer count = 0;

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            for (int i = 0; i < 10000; i++) {
                atomicInteger.getAndIncrement();
                count++;
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("atomicInteger=" + atomicInteger);
        System.out.println("count=" + count);
    }
}
atomicInteger=20000
count=13409

Process finished with exit code 0
  • Мы видим, что в это времяAtomicIntegerрезультат правильный

  • а такжеIntegerРезультат неверный, если вы хотите обеспечить потокобезопасность, вам нужно заблокировать

  • Пока он не вызывается несколько раз в потоке, безопасность потока может быть гарантирована, а атомарный класс с одним методом гарантирует безопасность потока.

  • Примечание. Атомарная операция + Атомарная операция! = Атомарная операция

1.3 Atomic*ArrayАнализ типа массива

  • кAtomicIntegerArrayНапример

  • Когда это тип массива, он гарантирует, что работа каждого элемента является потокобезопасной.

  • AtomicIntegerArrayметод иAtomicIntegerметоды похожи, ноAtomicIntegerArrayДля метода требуется указанный массивindex

  • Демонстрация кода:

/**
 * @author yiren
 */
public class AtomicIntegerArrayExample {
    private static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);

    public static void main(String[] args) throws InterruptedException {
        Runnable incrRunnable = () -> {
            for (int i = 0; i < atomicIntegerArray.length(); i++) {
                atomicIntegerArray.incrementAndGet(i);
            }
        };
        Runnable decrRunnable = () -> {
            for (int i = 0; i < atomicIntegerArray.length(); i++) {
                atomicIntegerArray.decrementAndGet(i);
            }
        };

        ExecutorService executorService = Executors.newFixedThreadPool(100);
        for (int i = 0; i < 1000; i++) {
            executorService.execute(incrRunnable);
        }
        for (int i = 0; i < 1000; i++) {
            executorService.execute(decrRunnable);
        }
        TimeUnit.SECONDS.sleep(5);
        for (int i = 0; i < atomicIntegerArray.length(); i++) {
            System.out.print(atomicIntegerArray.get(i) + " ");
        }
    }
}
0 0 0 0 0 0 0 0 0 0

1.4 AtomicReferenceАнализ типа приложения

  • AtomicReferenceроль класса иAtomicIntegerРазницы особой нет, просто изменился объект действия,AtomicIntegerсостоит в том, чтобы гарантировать атомарность целого числа, иAtomicReferenceэто сделать объект гарантией атомарности
  • а такжеAtomicReferenceбыло бы лучше, чемAtomicIntegerБолее мощный, потому что объект будет содержать много свойств, использование аналогично
  • метод объекта класса

  • кейс
/**
 * @author yiren
 */
public class SpinLock {
    private static AtomicReference<Thread> sign = new AtomicReference<>();

    private static void lock() {
        Thread current = Thread.currentThread();
        while (!sign.compareAndSet(null, current)) {
            System.out.println("fail to set!");
        }
    }

    private static void unlock() {
        Thread thread = Thread.currentThread();
        sign.compareAndSet(thread, null);
    }

    public static void main(String[] args) {
        Runnable runnable = () -> {
            System.out.println("start to get lock");
            SpinLock.lock();
            System.out.println("got lock successfully!");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                SpinLock.unlock();
            }
        };

        Thread thread = new Thread(runnable);
        Thread thread1 = new Thread(runnable);

        thread.start();
        thread1.start();
    }
}
  • мы используемAtomicReferenceреализовать спин-блокировку с помощьюcompareAndSetметод для сравнения и последующего назначения, чтобы избежать использования блокировок

1.5 Инкапсуляция общих типов в атомарные классы

  • кAtomicIntegerFieldUpdaterНапример

  • AtomicIntegerFieldUpdater.newUpdater(Counter.class, "count");Когда мы создаем объект, нам нужно указать целевой класс и свойства.

  • И при работе вам нужно передать объект операции

  • Почему это должно быть сделано именно так? без прямого изменения исходного объекта?

    • Если мы редко используем атомарные операции в кодировании, использование атомарных классов непосредственно в исходном объекте будет пустой тратой производительности.
    • Кроме того, когда мы используем классы, определенные другими, у нас есть такие требования, а у других таких требований нет, и мы не можем уничтожить чужие определения.AtomicIntegerFieldUpdaterИспользование , не нанесет вред исходному классу.
  • Примечание. Этот класс не поддерживаетstaticукрашенная переменная

  • Дело в следующем:

/**
 * @author yiren
 */
public class AtomicFieldUpdaterExample {
    private static Counter one = new Counter();
    private static Counter two = new Counter();
    private static AtomicIntegerFieldUpdater<Counter> updater = AtomicIntegerFieldUpdater.newUpdater(Counter.class, "count");


    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            for (int i = 0; i < 10000; i++) {
                one.count++;
                updater.getAndIncrement(two);
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("one.count = " + one.count);
        System.out.println("two.count = " + two.count);
    }

    private static class Counter {
        volatile int count;
    }
}
one.count = 18417
two.count = 20000

Process finished with exit code 0
  • Видно, что после обновления исходный поток операций с данными безопасен.

1.6 Adderаккумулятор

  • Возьмите LongAdder в качестве примера

  • LongAdderЭто новый класс, представленный в Java8, с высокой степенью параллелизма.LongAdderСравниватьAtomicLongЭффективность высока, и его способность использовать пространство для времени

  • Он фактически использует технологию блокировки сегментов,LongAdderИзменение разных потоков, соответствующих разным ячейкам, снижает вероятность конфликта и повышает производительность одновременного выполнения.

  1. Демонстрация кода: сравнениеAtomicLongа такжеLongAdder
/**
 * @author yiren
 */
public class AtomicLongExample {
    public static void main(String[] args) {
        AtomicLong counter = new AtomicLong();
        ExecutorService executorService = Executors.newFixedThreadPool(16);
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                counter.incrementAndGet();
            }
        };
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            executorService.execute(task);
        }
        executorService.shutdown();
        while (!executorService.isTerminated()) {
        }
        long end = System.currentTimeMillis();
        System.out.println("end-start=" + (end - start)+ "ms");

    }
}
end-start=2140ms

Process finished with exit code 0
/**
 * @author yiren
 */
public class LongAdderExample {
    public static void main(String[] args) {
        LongAdder counter = new LongAdder();
        ExecutorService executorService = Executors.newFixedThreadPool(16);
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        };
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            executorService.execute(task);
        }
        executorService.shutdown();
        while (!executorService.isTerminated()) {
        }
        long end = System.currentTimeMillis();
        System.out.println("end-start=" + (end - start)+ "ms");

    }
}
end-start=157ms

Process finished with exit code 0
  • Из приведенного выше видно, что моя локальная машина — это процессор i7, и единственная разница между двумя программами заключается в том, что используемые атомарные классы отличаются более чем в 10 раз.LongAdderзначительно больше, чемAtomicLongБыстрее
  1. Почему такая большая разница?
  • AtomicLongПосле завершения работы каждого потока ему нужно будет сбросить данные из локальной памяти потока в основную память, а затем другой поток должен сбросить новые данные из основной памяти.
  • а такжеLongAdderНе нужно этого делать,LongAdderКаждый поток будет иметь свой собственный счетчик, который используется для подсчета только внутри своего собственного потока, так что он не будет мешать счетчикам других потоков.
  • LongAdderВводится понятие сегментарного накопления, и существует внутренняяbaseпеременная иCell[] cellsМассивы участвуют в вычислении вместе
    • base: Если конкуренция не является жесткой, она будет напрямую накапливаться в этой переменной.
    • cells: Когда конкуренция жесткая, каждая нить разбрасывается и накапливается по-своему.cells[i]середина
  1. Применимая сцена
  • AtomicLong: сумма в низкой конкуренцииLongAdderАналогичен, но имеет метод CAS, обеспечивающий больше функциональности.
  • LongAdder: он имеет очевидные преимущества в случае высокой параллелизма, но применим только к сценам статистического суммирования и подсчета и имеет определенные ограничения.

1.7 Accumulatorаккумулятор

  • кLongAccumulatorНапример

  • Основное использование

/**
 * @author yiren
 */
public class AccumulatorExample {
    public static void main(String[] args) {
        // 累加 :此处的(left, right) -> left + right 可以替换成 Long::sum
        // left=3
        LongAccumulator longAccumulator = new LongAccumulator((left, right) -> left + right, 3);
        // left=3+right=3+2=5
        longAccumulator.accumulate(2);
        // left=5+right=5+3=8
        longAccumulator.accumulate(3);
        System.out.println(longAccumulator.getThenReset());
        // left=3
        LongAccumulator longAccumulator1 = new LongAccumulator((left, right) -> left - right, 3);
        // left=3-right=3-2=1
        longAccumulator1.accumulate(2);
        // left=1-right=1-3=-2
        longAccumulator1.accumulate(3);
        System.out.println(longAccumulator1.getThenReset());
        // 求最大值
        LongAccumulator longAccumulator2 = new LongAccumulator(Math::max, -1);
        longAccumulator2.accumulate(14);
        longAccumulator2.accumulate(3);
        System.out.println(longAccumulator2.getThenReset());

    }
}
8
-2
14

Process finished with exit code 0
  • Подробности смотрите в примечаниях
  • Некоторые люди могут найти это хлопотным. Это всего лишь один поток, а атомарный класс гарантированно работает в нескольких потоках. То есть мы можем напрямую вызывать разные потоки
/**
 * @author yiren
 */
public class AccumulatorExample01 {
    public static void main(String[] args) {
        LongAccumulator accumulator = new LongAccumulator((left, right) -> {
            long y = left;
            long x = right;
            return x + y;
        }, 0);

        ExecutorService executorService = Executors.newFixedThreadPool(100);
        IntStream.range(1, 100).forEach(item -> executorService.execute(() -> accumulator.accumulate(item)));
        executorService.shutdown();
        while (!executorService.isTerminated()) {

        }
        System.out.println(accumulator.get());
    }
}
4950

Process finished with exit code 0
  • используемые сцены:
    • Требуются параллельные вычисления, большой объем данных
    • заказ не требуется

2. Принцип КАС

2.1 Что такое КАС?

  • CAS, полное название - сравнение и обмен

  • CAS имеет три значения. Значение памяти Значение, ожидаемое значение Ожидаемое значение, которое нужно изменить Целевое, тогда и только тогда, когда Ожидаемое==Значение, значение памяти может быть изменено на Целевое, в противном случае ничего не делать. Наконец, верните текущее значение

  • В современных процессорах CAS может быть реализован со специальными инструкциями, а JVM при реализации также будет использовать инструкции по сборке: cmpxchg

2.2 Демонстрация случая

  • Эквивалентный код для CAS:
/**
 * @author yiren
 */
public class CasExample {
    private static volatile int value;

    public static synchronized int compareAndSwap(int expect, int target) {
        int oldValue = value;
        if (expect == oldValue) {
            value = target;
        }
        return value;
    }

    public static void main(String[] args) throws InterruptedException {
        value = 0;
        Runnable runnable = () -> {
            compareAndSwap(0, 1);
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println(value);
    }
}

2.3 Анализ исходного кода CAS

  • Возьмем в качестве примера исходный код атомарного класса AtomicInteger.

  • Ниже приведено определение свойства в AtomicInteger:

    // 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;
  • вvalueЭто атрибут, в котором мы в основном сохраняем данные; а valueOffset представляет адрес смещения текущего объекта в адресе памяти значения переменной value, потому что Unsafe получает исходное значение данных в соответствии с адресом смещения памяти, так что мы может внедрить CAS через небезопасный интервал

  • Давайте взглянемAtomicIntegerконкретный метод работы

   public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    public final int getAndDecrement() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
  • Видно, что основными родственными методами являютсяunsafeизcompareAndSwapInt,getAndAddIntИ каждый вызов метода передает текущий объект, адрес смещения значения и операнд

  • Небезопасный класс: это основной класс реализации CAS. Java не может напрямую обращаться к базовой операционной системе, но через локальные нативные методы. Тем не менее, JVM по-прежнему предоставляет способ. Небезопасный класс в JDK предоставляет атомы на аппаратном уровне.

  • Давайте посмотримint getAndAddInt(Object var1, long var2, int var4)метод

    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;
    }
  • var1 — это объект текущего AtomicInteger, а var2 — адрес смещения значения.UnsafeизgetIntVolatileПолучите значение текущего объекта AtomicInteger, а затем вызовитеUnsafeизcompareAndSwapIntспособ сделать CAS.
  • посмотриcompareAndSwapIntОпределение
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
  • Это нативный метод, метод реализации JVM на C++, который фактически вызывается, а код C++ вызываетсяAtomic::cmpxchg, а в современных процессорах это может фактически соответствовать инструкциям сравнения и обмена в наборе инструкций сборкиCMPXCHG

2.5 Недостатки CAS

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

Например

  • Начальное значение равно 0, поток 1 изменяет его на 1, а затем снова меняет на 0.
  • Поток 2 получает 0 до того, как поток 1 изменится на 1, а затем сравнивает его после того, как поток 1 снова изменится на 0.
  • В этот момент поток 2 будет успешно изменен, но поток 2 не знает, что поток 1 изменил номер в нем.
  • Для этого можно использовать номера версий для решения, например 1A-> 2B-> 3A-4B, каждая операция имеет номер версии в качестве записи. При сравнении используйте номер версии для сравнения
  • В Java есть AtomicStampedReference, который можно использовать для решения проблем ABA.
  1. В случае высокого параллелизма эффективность очень низкая, и может потребоваться много сравнений.

  • Источник содержания статьи:
  • «Искусство параллельного программирования на Java», исходный код версии JDK1.8, курс MOOC Wukong JUC

  • Ставьте палец вверх, если считаете, что сможете 👍 Спасибо!

обо мне

  • Координатор Ханчжоу, специализирующийся в области компьютерных наук и технологий в общеобразовательных колледжах и университетах.
  • Окончил 20 лет, в основном занимается внутренней разработкой стека технологий Java.
  • GitHub: github.com/imyiren
  • Blog : imyi.ren