Почему существует AtomicReference?

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

Я скомпилировал свои предыдущие статьи на Github, приветствую всех в звездахGitHub.com/Next Day Picks/Не голоден…

Мы узнали об атомарных классах инструментов, таких как AtomicInteger, AtomicLong и AtomicBoolean, раньше.java.util.concurrent.atomicИнструментальный класс под пакетом.

Содержимое, связанное с AtomicInteger, AtomicLong, AtomicBoolean, см.

Волшебное путешествие Atomic XXX

Статья о понимании инструментов JDK, таких как AtomicReference, относительно скучна, и это не значит, что качество статьи снижается, потому что я хочу придумать полный набор всестороннего анализа bestJavaer, это обязательно быть неотделимым от понимания инструментов JDK.

Помните: технология должна быть долгосрочной.

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

Давайте снова поговорим о старомодной проблеме с учетной записью и постепенно введем использование AtomicReference через проблему с личным банковским счетом.Давайте сначала посмотрим на базовый класс личной учетной записи.

public class BankCard {

    private final String accountName;
    private final int money;

    // 构造函数初始化 accountName 和 money
    public BankCard(String accountName,int money){
        this.accountName = accountName;
        this.money = money;
    }
    // 不提供任何修改个人账户的 set 方法,只提供 get 方法
    public String getAccountName() {
        return accountName;
    }
    public int getMoney() {
        return money;
    }
    // 重写 toString() 方法, 方便打印 BankCard
    @Override
    public String toString() {
        return "BankCard{" +
                "accountName='" + accountName + '\'' +
                ", money='" + money + '\'' +
                '}';
    }
}

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

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

public class BankCardTest {

    private static volatile BankCard bankCard = new BankCard("cxuan",100);

    public static void main(String[] args) {

        for(int i = 0;i < 10;i++){
            new Thread(() -> {
                // 先读取全局的引用
                final BankCard card = bankCard;
                // 构造一个新的账户,存入一定数量的钱
                BankCard newCard = new BankCard(card.getAccountName(),card.getMoney() + 100);
                System.out.println(newCard);
                // 最后把新的账户的引用赋给原账户
                bankCard = newCard;
                try {
                    TimeUnit.MICROSECONDS.sleep(1000);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

В приведенном выше коде мы сначала объявляем глобальную переменную BankCard, которая определяетсяvolatileЦель модификации - быть видимым для других потоков после изменения ссылки.После того, как каждый плательщик вносит определенную сумму денег, меняется сумма выходного счета.Мы можем наблюдать результат вывода.

Как видите, мы ожидали, что окончательный результат будет 1100 юаней, но в итоге было зачислено только 900. Куда делись 200 юаней? Мы можем сделать вывод, что приведенный выше код не является потокобезопасной операцией.

В чем проблема?

Хотя каждый volatile может гарантировать, что сумма каждой учетной записи актуальна, из-за комбинированной операции на вышеуказанных шагах, а именно获取账户引用а также更改账户引用, хотя каждая отдельная операция является атомарной, в сочетании она не является атомарной. Так что окончательный результат будет необъективным.

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

Можно видеть, что окончательный результат может быть таким, потому что после того, как поток t1 получает последнее изменение учетной записи, поток переключается на t2, t2 также получает последний статус учетной записи, а затем переключается на t1, t1 изменяет ссылку, поток переключается на t2 , а t2 изменяет ссылку, поэтому значение ссылки учетной записи изменяется.两次.

Так как же обеспечить потокобезопасность между получением ссылки и изменением ссылки?

Самый простой и грубый способ - использовать его напрямуюsynchronizedКлючевое слово заблокировано.

Используйте синхронизированный для обеспечения потокобезопасности

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

public class BankCardSyncTest {

    private static volatile BankCard bankCard = new BankCard("cxuan",100);

    public static void main(String[] args) {
        for(int i = 0;i < 10;i++){
            new Thread(() -> {
                synchronized (BankCardSyncTest.class) {
                    // 先读取全局的引用
                    final BankCard card = bankCard;
                    // 构造一个新的账户,存入一定数量的钱
                    BankCard newCard = new BankCard(card.getAccountName(), card.getMoney() + 100);
                    System.out.println(newCard);
                    // 最后把新的账户的引用赋给原账户
                    bankCard = newCard;
                    try {
                        TimeUnit.MICROSECONDS.sleep(1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

По сравнению с BankCardTest BankCardSyncTest добавляет синхронизированную блокировку.После запуска BankCardSyncTest мы обнаружили, что можем получить правильный результат.

Модифицируя BankCardSyncTest.class в объект bankCard, мы обнаружили, что потокобезопасность также может быть обеспечена, поскольку в этой программе будет изменена только bankCard, а других общих данных не будет.

Если есть другие общие данные, нам нужно использовать BankCardSyncTest.clas для обеспечения безопасности потоков.

Кроме,java.util.concurrent.atomicAtomicReference под пакетом также может гарантировать потокобезопасность.

Давайте сначала познакомимся с AtomicReference, а затем воспользуемся AtomicReference, чтобы переписать приведенный выше код.

Понимание атомарных ссылок

Потокобезопасность с AtomicReference

Давайте перепишем пример выше

public class BankCardARTest {

    private static AtomicReference<BankCard> bankCardRef = new AtomicReference<>(new BankCard("cxuan",100));

    public static void main(String[] args) {

        for(int i = 0;i < 10;i++){
            new Thread(() -> {
                while (true){
                    // 使用 AtomicReference.get 获取
                    final BankCard card = bankCardRef.get();
                    BankCard newCard = new BankCard(card.getAccountName(), card.getMoney() + 100);
                    // 使用 CAS 乐观锁进行非阻塞更新
                    if(bankCardRef.compareAndSet(card,newCard)){
                        System.out.println(newCard);
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

В приведенном выше примере кода мы использовали AtomicReference для переноса ссылки на BankCard, а затем использовалиget()Метод получает атомарную ссылку, а затем использует оптимистическую блокировку CAS для неблокирующего обновления.Стандарт обновления заключается в том, что если значение, полученное с помощью bankCardRef.get(), равно значению памяти, средства на счете банковской карты будет + 100. Наблюдаем Выводим результат.

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

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

Разобравшись с приведенным выше примером, давайте взглянем на использование AtomicReference.

AtomicReference и AtomicInteger очень похожи, они используют следующие три внутренних свойства.

wTiyJH.png

Unsafeдаsun.miscДля классов ниже пакета AtomicReference в основном полагается на некоторые собственные методы, предоставляемые sun.misc.Unsafe для обеспечения работы.原子性.

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

valueЯвляется ли фактическим значением в AtomicReference, из-за volatile это значение фактически является значением памяти.

Разница в том, что AtomicInteger — это инкапсуляция целых чисел, а AtomicReference соответствует обычному对象引用. То есть он гарантирует безопасность потоков при изменении ссылок на объекты.

get and set

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

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

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

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

ленивый метод

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

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

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

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

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

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

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

метод getAndSet

Атомарно установить заданное значение и вернуть старое значение. Его исходный код выглядит следующим образом

это позвонитunsafeВ методе getAndSetObject исходный код выглядит следующим образом

Вы можете видеть, что этот метод getAndSet включает в себя два метода, реализованных cpp, один из нихgetObjectVolatile,одинcompareAndSwapObjectметод, они используются в цикле do...while, то есть значение последней ссылки на объект каждый раз получается первым, и если два объекта успешно обмениваются с помощью CAS, он возвращается напрямуюvar5Значением var5 должно быть значение памяти до обновления, то есть старое значение.

метод compareAndSet

Это самый ключевой метод CAS AtomicReference.В отличие от AtomicInteger, AtomicReference вызываетсяcompareAndSwapObject, в то время как AtomicInteger вызываетcompareAndSwapIntметод. Реализация этих двух методов выглядит следующим образом.

путь вhotspot/src/share/vm/prims/unsafe.cppсередина.

Мы уже разбирали исходный код AtomicInteger, поэтому давайте проанализируем исходный код AtomicReference.

Поскольку объект существует в куче, методindex_oop_from_field_offset_longЭто должно быть получить адрес памяти объекта, а затем использоватьatomic_compare_exchange_oopспособ выполнения CAS-обмена объектами.

Этот код сначала определит, следует ли использоватьUseCompressedOops, это,指针压缩.

Вот краткое объяснение концепции сжатия указателя: JVM изначально была 32-битной, но с появлением 64-битной JVM это также создает проблему, занимаемая память больше, но память JVM не должна превышать 32G. , В целях экономии места после версии JDK 1.6 мы можем включить JVM в 64-битной指针压缩(UseCompressedOops)Чтобы уменьшить размер нашего указателя объекта, чтобы помочь нам сэкономить место в памяти, в JDK 8 эта директива включена по умолчанию.

Если сжатие указателя не включено, 64-битная JVM будет использовать 8 байтов (64 бита) для хранения реального адреса памяти, что более проблематично, чем предыдущее использование 4 байтов (32 бита) для сжатия адреса памяти:

  1. Увеличение нагрузки на сборщик мусора: ссылки на 64-разрядные объекты занимают больше места в куче, оставляя меньше места для других данных. Это ускоряет возникновение GC и выполняет GC чаще.
  2. Уменьшите частоту попаданий в кеш ЦП: увеличивается ссылка на 64-битный объект, ЦП может кэшировать меньше операций, что снижает эффективность кеша ЦП.

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

Как видите, нижний слой метода atomic_compare_exchange_oop также используется.Atomic:cmpxchgМетод выполняет обмен CAS, а затем декодирует и возвращает старое значение (мои ограниченные знания C++ могут быть разобраны только здесь, если вы понимаете этот код, скажите, пожалуйста, позвольте мне задать волну)

метод weakCompareAndSet

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

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

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

Суммировать

В этой статье в основном представлены предыстория AtomicReference, сценарии использования AtomicReference, а также исходный код AtomicReference и анализ исходного кода ключевых методов. Эта статья AtomicReference в основном охватывает весь контент об AtomicReference в Интернете.К сожалению, исходный код cpp может быть плохо проанализирован, что требует достаточных знаний в области программирования C/C++.Если есть читатели и друзья, у которых есть последние исследования, результаты, пожалуйста дайте мне знать вовремя.

Кроме того, добавьте мой WeChat becxuan, присоединяйтесь к группе ежедневных вопросов, делитесь одним вопросом интервью каждый день, пожалуйста, обратитесь к моему Github для получения дополнительной информации, станьте лучшим лучшимJavaer, эта статья была включена, см. оригинальную ссылку для получения подробной информации.

Я лично перелил шесть PDF-файлов. После того, как программист поиска WeChat cxuan обратил внимание на официальный аккаунт, он ответил cxuan в фоновом режиме и получил все PDF-файлы.Эти PDF-файлы следующие

Шесть ссылок в формате PDF