Открою вам большой секрет про AtomicInteger

Java задняя часть

i++ не является потокобезопасной операцией, потому что это не атомарная операция.

Итак, если я хочу добиться такого же эффекта, как i++, какие коллекции или служебные классы мне следует использовать?

До JDK1.5, чтобы гарантировать, что определенный基本тип данных или引用Атомарность операций с типами данных должна основываться на иностранных ключевых словах.synchronized, но эта ситуация изменилась после JDK1.5. Конечно, вы все еще можете использовать synchronized для обеспечения атомарности. Поточно-ориентированный способ, о котором мы говорим здесь, — это атомарные классы инструментов, такие какAtomicInteger, AtomicBooleanЖдать. Эти атомарные классы являются потокобезопасными служебными классами, и они такжеLock-Freeиз. Давайте посмотрим на эти классы инструментов и на то, что такое концепция Lock-Free.

Узнать об AtomicInteger

AtomicIntegerЭто недавно добавленный инструментальный класс в JDK 1.5. Давайте сначала рассмотрим его отношения наследования.

AtomicInteger01

Как и класс-оболочка Integer для int, он наследуется отNumberКатегория.

AtomicInteger02

Класс Number — это класс-оболочка для базовых типов данных.Как правило, объекты, связанные с типами данных, наследуются от класса Number.

Его система наследования очень проста, давайте рассмотрим ее основные свойства и методы.

Основные свойства AtomicInteger

Есть три основных свойства AtomicInteger

wTiyJH.png

Unsafeдаsun.miscКласс ниже пакета, AtomicInteger, в основном полагается на некоторые собственные методы, предоставляемые sun.misc.Unsafe, для обеспечения атомарности операций.

НебезопасноobjectFieldOffsetМетод может получить смещение адреса свойства члена в памяти относительно адреса памяти объекта. Проще говоря, это найти адрес этой переменной в памяти, что удобно для последующих операций непосредственно через адрес памяти.value

Мы поговорим об этом позже

valueявляется значением AtomicIneger.

Конструктор AtomicInteger

Продолжая смотреть вниз, есть только два конструктора AtomicInteger.Один является конструктором без параметров.Значение конструктора без параметров по умолчанию равно 0. Конструктор с параметрами может указывать начальное значение.

AtomicInteger04

Методы в AtomicInteger

Давайте поговорим о методах в AtomicInteger.

Получить и установить

Давайте сначала рассмотрим простейшие методы get и set:

get(): получить значение текущего AtomicInteger

set(): установить значение текущего AtomicInteger

get() может атомарно считывать данные в AtomicInteger, а set() может атомарно устанавливать текущее значение, потому что и get(), и set() в конечном итоге воздействуют на переменную значения, а значение определяетсяvolatileМодифицировано, поэтому получение и установка эквивалентны чтению и настройке памяти. Как показано ниже

AtomicInteger05

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

Инкрементная операция

AtomicInteger вIncrementalСвязанные методы могут удовлетворить наши потребности

  • getAndIncrement(): атомарно увеличивает текущее значение и возвращает результат. эквивалентноi++операция.
image-20200911085857825

Чтобы проверить, является ли он потокобезопасным, мы используем следующий пример для проверки

public class TAtomicTest implements Runnable{

    AtomicInteger atomicInteger = new AtomicInteger();

    @Override
    public void run() {
        for(int i = 0;i < 10000;i++){
            System.out.println(atomicInteger.getAndIncrement());
        }
    }
    public static void main(String[] args) {

        TAtomicTest tAtomicTest = new TAtomicTest();

        Thread t1 = new Thread(tAtomicTest);
        Thread t2 = new Thread(tAtomicTest);
        t1.start();
        t2.start();
    }

}

Вы обнаружите, что это потокобезопасная операция, выводя результат, вы можете изменить значение i, но окончательный результат по-прежнему равен i - 1, потому что сначала берется значение, а затем + 1, его схематическая диаграмма выглядит так: следует.

AtomicInteger06
  • incrementAndGetНапротив, сначала выполняется операция + 1, а затем возвращается результат с автоматическим приращением, что обеспечивает атомарную операцию над значением. Как показано ниже
AtomicInteger07

Декрементная операция

Напротив, операция уменьшения, такая как x-- или x = x-1, является атомарной. Мы все еще можем использовать методы в AtomicInteger для замены

  • getAndDecrement: возвращает значение int текущего типа, затем уменьшает значение value. Ниже приведен тестовый код
class TAtomicTestDecrement implements Runnable{

    AtomicInteger atomicInteger = new AtomicInteger(20000);

    @Override
    public void run() {
        for(int i = 0;i < 10000 ;i++){
            System.out.println(atomicInteger.getAndDecrement());
        }
    }

    public static void main(String[] args) {

        TAtomicTestDecrement tAtomicTest = new TAtomicTestDecrement();

        Thread t1 = new Thread(tAtomicTest);
        Thread t2 = new Thread(tAtomicTest);
        t1.start();
        t2.start();

    }

}

Ниже приведена схема getAndDecrement.

AtomicInteger08
  • decrementAndGet: Точно так же метод decrementAndGet заключается в том, чтобы сначала выполнить операцию уменьшения, а затем получить значение значения.Схема выглядит следующим образом.
AtomicInteger09

Метод LazySet

Знаете ли вы, что у volatile есть барьеры памяти?

Что такое барьер памяти?

Барьер памяти, также известный как内存栅栏, барьер памяти, барьерная инструкция и т. д. — тип барьерной инструкции синхронизации, которая представляет собой точку синхронизации в работе ЦП или компилятора при произвольном доступе к памяти, так что все операции чтения и записи до этой точки могут быть выполнены прежде чем они могут быть выполнены Действия после этой точки. Это также способ сделать состояние памяти процессора ЦП видимым для других процессоров.

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

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

Накладные расходы на барьеры памяти очень легкие, но какими бы маленькими они ни были, накладные расходы есть.LazySet как раз то, что он делает.Он будет читать и записывать переменные в виде обычных переменных.

Можно также сказать, что:Лень ставить барьеры

AtomicInteger10

Метод GetAndSet

Атомарно установить заданное значение и вернуть старое значение.

Его исходный код заключается в вызове метода getAndSetInt в небезопасном режиме, как показано ниже.

AtomicInteger11

сначала зациклить, а затем вызватьgetIntVolatileметод, я не нашел этот метод в cpp, друзья, которые нашли его, не забудьте сообщить мне вовремя, чтобы изучить его.

Цикл до тех пор, пока compareAndSwapInt не вернет false, что означает, что использование CAS не обновило новое значение, поэтому var5 возвращает последнее значение памяти.

CAS-метод

CAS, о котором мы говорили, на самом делеCompareAndSetМетод, как следует из названия,Сравните и обновитеСмысл , конечно, это буквальное понимание, а буквальное понимание немного предвзятое.На самом деле то, что они означают, это сначала сравнить, а затем обновить, если устраивает.

AtomicInteger12

Исходный код уровня CAS Java приведен выше, а официальное объяснение JDK для него —Если текущее значение равно значению expect , то атомарно установите текущее значение для обновления заданного значения., этот метод вернет логический тип, если это правда, это означает, что сравнение и обновление прошли успешно, в противном случае это означает сбой.

CAS также является механизмом параллелизма без блокировки, также известным какLock Free, так вы думаете, Lock Free это здорово? нисколько.

Далее строим замок и разблокируемCASLock

class CASLock {

    AtomicInteger atomicInteger = new AtomicInteger();
    Thread currentThread = null;

    public void tryLock() throws Exception{

        boolean isLock = atomicInteger.compareAndSet(0, 1);
        if(!isLock){
            throw new Exception("加锁失败");
        }

        currentThread = Thread.currentThread();
        System.out.println(currentThread + " tryLock");

    }

    public void unlock() {

        int lockValue = atomicInteger.get();
        if(lockValue == 0){
            return;
        }
        if(currentThread == Thread.currentThread()){
            atomicInteger.compareAndSet(1,0);
            System.out.println(currentThread + " unlock");
        }
    }

    public static void main(String[] args) {

        CASLock casLock = new CASLock();

        for(int i = 0;i < 5;i++){

            new Thread(() -> {
                try {
                    casLock.tryLock();
                    Thread.sleep(10000);
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    casLock.unlock();
                }
            }).start();
        }

    }
}

В приведенном выше коде мы создаем CASLock, вtryLockВ методе мы сначала используем метод CAS для обновления, если обновление не удается, генерируется исключение, и текущий поток устанавливается в качестве потока блокировки. существуетunLockВ методе мы сначала оцениваем, равно ли текущее значение 0, если оно равно 0, это результат, который мы хотим увидеть, и возвращаемся напрямую. В противном случае это 1, что означает, что текущий поток все еще заблокирован.Давайте оценим, является ли текущий поток заблокированным потоком, и если да, выполните операцию разблокировки.

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

// 伪代码

// 当前值
int v = 0;
int a = 0;
int b = 1;

if(compare(0,0) == true){
  set(0,1);
}
else{
  // 继续向下执行
}

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

Без лишних слов, вот принципиальная схема compareAndSet

AtomicInteger14
  • weakCompareAndSet: Блин, я несколько раз перечитал очень серьезно, и обнаружил, что этот метод JDK1.8 точно такой же, как метод compareAndSet. . .
AtomicInteger15

Но так ли это на самом деле? Нет, исходный код JDK очень обширен и глубок, поэтому он не будет разрабатывать повторяющийся метод.Вы думаете, что команда JDK не будет фиксировать такую ​​​​низкоуровневую команду, но в чем причина?

Книга "Подробное объяснение Java High Concurrency" дает нам ответ

AtomicInteger16

AddAndGet

AddAndGet и getAndIncrement, getAndAdd, incrementAndGet и другие методы используют операцию do ... while + CAS, которая фактически эквивалентна спин-блокировке. возвращается. Схема выглядит следующим образом

AtomicInteger17

Погрузитесь в AtomicInteger

Мы обсуждали конкретное использование AtomicInteger выше и знаем, что AtomicInteger полагается на volatile и CAS для обеспечения атомарности, поэтому давайте проанализируем, почему CAS может гарантировать атомарность и каков его базовый уровень? Какое отношение AtomicInteger имеет к оптимистичной блокировке?

Основной принцип реализации AtomicInteger

Давайте посмотрим на этого милогоcompareAndSetL(CAS)метод, почему атомарность гарантируется только для этих двух строк кода?

AtomicInteger18

Мы видим, что этот метод CAS эквивалентен вызову unsafe вcompareAndSwapIntметод, давайте перейдем к небезопасному, чтобы увидеть конкретную реализацию.

AtomicInteger19

сравнитеAndSwapIntsun.miscметод в , этот метод являетсяnativeметод, его нижний слой реализован на C/C++, поэтому нам нужно посмотреть исходный код C/C++.

Знаете ли вы, в чем прелесть C/C++? Использование Java — это игра с приложением и архитектурой, а C/C++ — игра с сервером и нижним уровнем.

Исходный код compareAndSwapInt находится вjdk8u-dev/hotspot/src/share/vm/prims/unsafe.appПо пути его реализация исходного кода

AtomicInteger20

этоUnsafe_CompareAndSwapIntметод, мы находим этот метод

AtomicInteger21

Я не могу понять исходный код C/C++, но это не мешает нам найти ключевой кодAtomic::cmpxchg, cmpxchg — это ассемблерная инструкция для архитектуры процессора x86, ее основная функция — сравнение и обмен операндами. Давайте продолжим вниз, чтобы найти определение этой команды.

Мы обнаружим, что для разных ОС базовая реализация отличается.

AtomicInteger22

Мы нашли реализацию Windows следующим образом

AtomicInteger23

Продолжаем смотреть вниз, он собственно и определяет код на 216 строке, находим его

AtomicInteger24

На этом этапе требуется знание инструкций по сборке и регистров.

вышеos::is-MP()— это интерфейс многопроцессорной операционной системы, ниже — __asm, ключевое слово C/C++, используемое для вызова встроенного ассемблера.

Код на __asm ​​представляет собой ассемблер, который грубо раскладывает значения dest, exchange_value и compare_value в регистры.LOCK_IF_MPОбщий смысл кода таков.

AtomicInteger25

Если это мультипроцессор, будет выполнена блокировка, а затем будет выполнена операция сравнения. Среди них cmp означает сравнение, а mp означает, чтоMultiProcess,jeПредставляет равный переход, а L0 представляет бит флага.

Возвращаемся к приведенной выше инструкции по сборке, мы видим, что нижний слой CAScmpxchgинструкция.

оптимистическая блокировка

У вас есть этот вопрос, почему AtomicInteger может получить текущее значение, тогда почему оно все еще появляетсяexpectValueиvalueА несоответствия?

Поскольку AtomicInteger — это всего лишь атомарный служебный класс, он не является исключительным, он не похож наsynchronizedилиlockТак же, как взаимоисключающие и исключающие, помните, что в AtomicInteger есть два метода get и set? они просто используютvolatile Modified и volatile не являются атомарными, поэтому могут быть несоответствия между текущим значением expectValue и value, поэтому могут возникать повторные модификации.

Есть два решения вышеописанной ситуации, одно из них — использоватьsynchronizedиlockПодобный механизм блокировки, этот тип блокировки является эксклюзивным, то есть только один поток может изменять его одновременно.Этот метод может гарантировать атомарность, но относительные накладные расходы относительно велики.Такой тип блокировки также называется пессимистической блокировкой. . Другим решением является использование版本号илиCAS 方法.

номер версии

Механизм номера версии заключается в добавленииversionЭто реализуется полем, указывающим, сколько раз данные были изменены.Когда операция записи выполнена и запись прошла успешно, версия = версия + 1. Когда поток А захочет обновить данные, он также прочитает значение версии при чтении данных.При отправке обновления, если только что прочитанное значение версии равно значению версии в текущей базе данных, оно будет обновлено; в противном случае операция обновления будет повторяться до тех пор, пока обновление не будет успешным.

CAS-метод

Другой способ - CAS. Мы использовали много места, чтобы представить метод CAS. Тогда мы думаем, что теперь у вас есть определенное понимание механизма его работы, и мы не будем объяснять его механизм работы.

У всего есть свои преимущества и недостатки.В индустрии программного обеспечения нет идеального решения, есть только оптимальное решение.Поэтому оптимистическая блокировка также имеет свои недостатки и недостатки, то есть проблему ABA.

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

Проблема ABA заключается в том, что если значение переменной, прочитанной в первый раз, равно A, а когда она готова к записи в A, обнаруживается, что значение по-прежнему равно A, то в этом случае можно считать, что значение переменной равно A. значение A не изменилось. ? Это может быть случай A -> B -> A , но AtomicInteger так не думает, он просто верит в то, что видит, и видит то, что видит. Например

Если теперь есть односвязный список, как показано на следующем рисунке

AtomicInteger26

A.next = B, B.next = null, в это время два потока T1 и T2 извлекают из односвязного списка соответственно A. По каким-то особым причинам T2 изменяет A на B, а затем снова на A. При этом время, T1 Выполните метод CAS и обнаружите, что односвязный список по-прежнему A, и метод CAS будет выполнен.Хотя результат правильный, эта операция вызовет некоторые потенциальные проблемы.

AtomicInteger27

В настоящее время это все еще односвязный список.Два потока T1 и T2 соответственно извлекают A из односвязного списка, а затем T1 изменяет связанный список на ACD, как показано на следующем рисунке.

AtomicInteger28

В это время T2 обнаруживает, что значение памяти по-прежнему равно A, и попытается заменить значение A значением B, поскольку ссылка B равна нулю, что приведет к тому, что C и D в это время будут в свободном состоянии.

AtomicInteger29

JDK 1.5 и вышеAtomicStampedReference класс предоставляет эту возможность, в которойcompareAndSetМетод заключается в том, чтобы сначала проверить, равно ли текущее значение ожидаемому значению. Критерием для оценки является то, что текущая ссылка и штемпель равны ожидаемой ссылке и почтовому штемпелю соответственно. Если они все равны, они атомарно устанавливаются в заданное значение обновления.

AtomicInteger30

Что ж, приведенный выше поток кода Java Увидев натив, мы понимаем, что снова собираемся шлепнуть cpp. открыть

AtomicInteger31

Просто объяснитеUnsafeWrapperЭто просто обертка, просто другое имя. Затем, после некоторой обработки JNI, поскольку compareAndSwapOject сравнивает ссылки, он должен пройти объектно-ориентированное преобразование C++. Основным методом являетсяatomic_compare_exchange_oop

AtomicInteger32

Как видите, снова появляются знакомые словаcmpxchg, то есть compareAndSwapOject использует атомарную инструкцию cmpxchg, но она претерпела ряд преобразований.

постскриптум

Возникает вопрос, может ли CAS гарантировать видимость между переменными? Зачем?

еще есть вопрос,getIntVolatileГде источник cpp метода? Как его найти?

Если большие ребята выше заинтересованы в этих двух вопросах, добро пожаловать в общение.

Обратите внимание на официальный аккаунт.Программист cxuan отвечает на cxuan для получения качественной информации.

Я сам написал шесть PDF-файлов, очень хардкорно, ссылки ниже

Я сам написал шесть PDF-файлов, очень хардкорно, ссылки ниже

Я сам написал шесть PDF-файлов, очень хардкорно, ссылки ниже

Cxuan кропотливо разработал четыре PDF-файла.

Cxuan получил еще два PDF-файла