Все еще используете Synchronized? Вы понимаете атомный?

Java задняя часть GitHub Безопасность

предисловие

Только лысина может стать сильнее

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

多线程文章

чтениеПосле прочтения "Руководства по разработке Java для Alibaba", остались нерешенные вопросы:

Если это операция count++, используйте следующий класс для реализации: AtomicInteger count = new AtomicInteger(); count.addAndGet(1); Если это JDK8, рекомендуется использовать объект LongAdder, который имеет лучшую производительность, чем AtomicLong (уменьшает количество оптимистичных попыток блокировки).

Я много раз видел класс AtomicInteger, когда учился, но я никогда не делал соответствующих заметок. Теперь у меня есть проблема, поэтому я пришел, чтобы написать записку, и надеюсь, что вРешайте проблемы во время обучения.

1. Основная мощение

Сначала возьмем пример:


public class AtomicMain {

    public static void main(String[] args) throws InterruptedException {

        ExecutorService service = Executors.newCachedThreadPool();

        Count count = new Count();
        // 100个线程对共享变量进行加1
        for (int i = 0; i < 100; i++) {
            service.execute(() -> count.increase());
        }

        // 等待上述的线程执行完
        service.shutdown();
        service.awaitTermination(1, TimeUnit.DAYS);


        System.out.println("公众号:Java3y---------");
        System.out.println(count.getCount());
    }

}

class Count{

    // 共享变量
    private Integer count = 0;
    public Integer getCount() {
        return count;
    }
    public  void increase() {
        count++;
    }
}

Можете ли вы угадать, каков результат? Это 100?

Запустите несколько раз, чтобы найти:результат не определен, может быть 95, может быть 98, может быть 100

结果不确定

По результатам мы знаем, что приведенный выше код являетсяпоток небезопасениз! Если код потокобезопасен, результаты многократного выполнения согласуются!

Мы можем найти проблему:count++ине атомыработать. так какcount++нужно пройти读取-修改-写入три шага. Например:

  • Если в какой-то момент: значение count, прочитанное потоком A, равно 10, и значение count, прочитанное потоком B, также равно 10
  • Тема Параcount++, значение count в это время равно 11
  • Пара резьбы Bcount++, значение count в это время также равно 11 (поскольку счетчик, прочитанный потоком B, равен 10)
  • Итак, здесь вы должны знать, почему наши результаты неопределенны.

Сделать приведенный выше код потокобезопасным (каждый раз результат равен 100) тоже очень просто, ведь мы же люди, изучившие синхронизированные блокировки:

  • существуетincrease()Просто добавьте синхронизированную блокировку

public synchronized void increase() {
    count++;
}

Результат равен 100 независимо от того, сколько раз он выполняется:

结果都是100

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

  • Синхронизированные блокировки являются эксклюзивными, что означает, что если выполняются другие потоки, текущий поток может только ждать!

так что мыатомарная переменнаяКласс здесь!

1.2CAS Давайте посмотрим

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

Источник Википедия:

сравнить и поменять местами (CAS), даатомТип операции, которую можно использовать для реализации в многопоточном программировании.Бесперебойный обмен данными, чтобы избежать проблемы несогласованности данных, вызванной неопределенностью порядка выполнения и непредсказуемостью прерывания, когда несколько потоков перезаписывают определенные данные одновременно. Эта операция сравнивает значение в памяти с указанными данными и заменяет данные в памяти новым значением, если значение совпадает.

CAS имеет 3 операнда:

  • значение памяти В
  • старое ожидаемое значение A
  • новое значение B для изменения

Когда несколько потоков пытаются использовать CAS для одновременного обновления одной и той же переменной,Только один из потоков может обновить значение переменной(Когда A и значение памяти V одинаковы, измените значение памяти V на B), и другие потоки терпят неудачу, отказавший поток не будет приостановлен, но ему будет приказано потерпеть неудачу в этом соревновании.и можно попробовать еще раз (или ничего не делать).

Нарисуем картинку, чтобы понять:

CAS理解

Мы можем обнаружить, что CAS имеет две ситуации:

  • Если значение памяти V и наше ожидаемое значение Aравный, затем измените значение памяти на B, операция выполнена успешно!
  • Если значение памяти V и наше ожидаемое значение Aне равный, обычно бывает два случая:
    • повторить (прокрутить)
    • ничего не делать

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

1.2.1 Повторная попытка отказа CAS (прокрутка)

Например, я использовал 100 потоков выше, чтобы увеличить значение счетчика на 1. Мы все знаем, что если это потокобезопасно, конечным результатом этого значения счетчика должно быть 100. Это означает:Каждый поток существенно увеличивает значение счетчика на 1..

Я продолжаю рисовать картинку, чтобы проиллюстрировать, как CAS повторяет попытку (зацикливается и пытается снова):

CAS循环重试

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

1.2.2 Сбой CAS ничего не делает

Выше было сказано, что каждый поток должен добавлять 1 к значению счетчика, но у нас также может быть такая ситуация:установите значение счетчика на 5

Я также нарисую картинку для иллюстрации:

CAS失败什么都不做

Суть понимания CAS заключается в следующем:CAS является атомарным, хотя вы можете увидеть сравнение и обмен и подумать, что будет две операции, но ведь это атомарно!

Во-вторых, краткое введение в класс атомарных переменных

Класс атомарной переменной находится вjava.util.concurrent.atomicПод пакетом вообще столько:

原子变量类

Мы можем классифицировать его:

  • основной тип:
    • AtomicBoolean: логическое значение
    • AtomicInteger: целое число
    • AtomicLong: длинное целое
  • множество:
    • AtomicIntegerArray: целые числа в массиве
    • AtomicLongArray: длинное целое в массиве.
    • AtomicReferenceArray: тип ссылки в массиве.
  • Тип ссылки:
    • AtomicReference: ссылочный тип
    • AtomicStampedReference: Тип ссылки с номером версии
    • AtomicMarkableReference: ссылочный тип с маркерным битом
  • Свойства объекта:
    • AtomicIntegerFieldUpdater: свойство объекта является целым числом
    • AtomicLongFieldUpdater: свойство объекта представляет собой длинное целое число.
    • AtomicReferenceFieldUpdater: свойство объекта является ссылочным типом
  • JDK8 добавляет DoubleAccumulator, LongAccumulator, DoubleAdder, LongAdder
    • Является улучшением таких классов, как AtomicLong. Например, LongAccumulator и LongAdder более эффективны, чем AtomicLong, в среде с высокой степенью параллелизма.

Классы в пакете Atomic в основном используютсяUnsafeРеализован класс-оболочка.

В Unsafe (CAS) нам нравится несколько методов:


// 第一和第二个参数代表对象的实例以及地址,第三个参数代表期望值,第四个参数代表更新值
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

Общий обзор таков: реализация класса пакета Atomic вызывает методы Unsafe, а нижний уровень Unsafe фактически вызывает код C, код C вызывает сборку и, наконец, генерируетодинИнструкция ЦП cmpxchg завершает операцию. Вот почему CAS является атомарным, потому что это инструкция ЦП, которая не будет прервана.

2.1 Использование класса атомарных переменных

Поскольку мы также упоминали выше, использование синхронизированных блокировок вызывает некоторую суету, давайте изменим это с помощью класса атомарных переменных:


class Count{

    // 共享变量(使用AtomicInteger来替代Synchronized锁)
    private AtomicInteger count = new AtomicInteger(0);
    
    public Integer getCount() {
        return count.get();
    }
    public void increase() {
        count.incrementAndGet();
    }
}


// Main方法还是如上

После модификации, сколько бы раз она не выполнялась, наш результат всегда будет 100!

На самом деле использование атомарных классов под пакетом Atomic не слишком отличается.Если вы разбираетесь в различных типах атомарных классов и смотрите на API, вы в основном будете их использовать (в интернете тоже написано более подробно, так что я тут решительно лень)...

2.2 Проблема АВА

Одним из недостатков использования CAS является проблема ABA. Сначала опишу словами:

  • теперь у меня есть переменнаяcount=10, теперь есть три потока, A, B, C
  • Поток A и поток C читают переменную count одновременно, поэтому значение памяти и ожидаемое значение потока A и потока C равны 10.
  • На этом этапе поток A использует CAS для изменения значения счетчика на 100.
  • После модификации в этот момент вошел поток B, прочитал значение count до 100 (значение памяти и ожидаемое значение равно 100) и изменил значение count на 10.
  • Поток C получает право на выполнение, обнаруживает, что значение памяти равно 10, ожидаемое значение также равно 10, а значение счетчика изменено на 11.

Вышеуказанные операции могут быть выполнены нормально, так что же произойдет? ? Поток C не может знать значение счетчика, измененное потоком A и потоком B, поэтомурискиз.

Позвольте мне нарисовать еще одну картинку, чтобы проиллюстрировать проблему ABA (в качестве примера возьмем связанный список):

CAS ABA的问题讲解

2.3 Решение проблемы АВА

Чтобы решить проблему ABA, мы можем использовать классы AtomicStampedReference и AtomicMarkableReference, которые предоставляет нам JDK.

AtomicStampedReference:

An {@code AtomicStampedReference} maintains an object referencealong with an integer "stamp", that can be updated atomically.

Проще говоря, он обеспечиваетВерсия, и если эта версия изменена, она автоматически обновляется.

Принцип, вероятно, таков: объект Pair сохраняется, а объект Pair хранит ссылку на наш объект и значение штампа. Каждый CAS сравнивает два объекта Pair.



	// Pair对象
    private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;

	// 比较的是Pari对象
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

Поскольку есть еще один номер версии для сравнения, проблем с ABA не будет.

2.4 Производительность LongAdder лучше, чем у AtomicLong

Если это JDK8, рекомендуется использовать объект LongAdder, который имеет лучшую производительность, чем AtomicLong (уменьшает количество оптимистичных попыток блокировки).

Я пошел проверить некоторые блоги и информацию, что, вероятно, означает:

  • При использовании AtomicLong большое количество потоков будет одновременно конкурировать за обновления при высокой степени параллелизма.та же атомарная переменная, но поскольку CAS только одного потока будет успешным одновременно, другие потоки будут продолжать пытаться вращаться и пытаться выполнять операции CAS, что приведет к трате большого количества ресурсов ЦП.
  • И LongAdder можно резюмировать следующим образом: внутреннее значение основных данныхотдельныйв массив (Cell), когда каждый поток обращается к нему, он сопоставляется с одним из чисел посредством хеширования и других алгоритмов подсчета, и конечным результатом подсчета является номер массива.суммировать и накапливать.
    • Проще говоря, это разброс значения по нескольким значениям, что можно делать одновременно.дисперсионное давление, производительность улучшилась.

Использованная литература:

Наконец

Использованная литература:

  • Заметки о параллельном программировании на Java — содержаниеBlog.csdn.net/ Пань Вэй, 19 ...
  • «Практика параллельного программирования на Java»
  • Искусство параллельного программирования на Java

Если вы считаете, что я написал хорошо, посмотрите:

  • настаиватьоригинальныйТехнический общедоступный номер: Java3y. Ответить 1 Присоединяйтесь к группе обмена Java
  • статьиНавигация по каталогу(Изысканная карта мозга + огромные видеоресурсы):GitHub.com/Zhongf UC очень…