Интервьюер: Вы понимаете оптимистическую блокировку и пессимистическую блокировку?

Java

предисловие

Оптимистичная блокировка и пессимистическая блокировка — часто задаваемые вопросы на интервью. В этой статье будут постепенно представлены их основные концепции, методы реализации (включая примеры), применимые сценарии и вопросы интервьюеров, с которыми вы можете столкнуться, в надежде помочь вам произвести впечатление на интервьюеров.

содержание

1. Основные понятия 2. Методы реализации (включая примеры) 3. Преимущества и недостатки и применимые сценарии 4. Интервьюер спросил: Заблокирована ли оптимистическая блокировка? 5. Интервьюер спросил: Каковы недостатки CAS? 6. Резюме

1. Основные понятия

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

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

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

2. Реализация (включая примеры)

Прежде чем объяснять, как это реализовать, необходимо прояснить:Оптимистическая блокировка и пессимистическая блокировка — это две идеи, и их использование очень обширно и не ограничивается определенным языком программирования или базой данных.

Пессимистическая блокировка реализуется блокировкой, которая может быть либо блокировкой блоков кода (например, ключевым словом synchronized в Java), либо блокировкой данных (например, монопольной блокировкой в ​​MySQL).

Существует два основных способа реализации оптимистической блокировки: механизм CAS и механизм номера версии, которые подробно описаны ниже.

1. CAS (сравнить и поменять местами)

Операция CAS состоит из 3 операндов:

  • Место в памяти для чтения и записи (V)

  • Ожидаемое значение для сравнения (A)

  • Новое значение для записи (B)

Логика работы CAS следующая: если значение ячейки памяти V равно ожидаемому значению A, обновить ячейку до нового значения B, иначе ничего не делать. Многие операции CAS цикличны: если операция не удалась, она будет повторяться до тех пор, пока операция не завершится успешно.

Это приводит к новому вопросу: поскольку CAS включает две операции, Compare и Swap, как она обеспечивает атомарность? Ответ таков: CAS — это атомарная операция, поддерживаемая ЦП, и ее атомарность гарантируется на аппаратном уровне.

Давайте возьмем операцию автоинкремента (i++) в Java в качестве примера, чтобы увидеть, как пессимистическая блокировка и CAS соответственно обеспечивают безопасность потоков. Мы знаем, что операция автоинкремента в Java не является атомарной операцией, она фактически состоит из трех независимых операций:

  • прочитать значение i;

  • плюс 1;

  • записать новое значение обратно в я

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

Запустите программу и используйте 1000 потоков для одновременного выполнения операций автоинкремента над значением 1, значением 2 и значением 3. Можно обнаружить, что значения значения 2 и значения 3 всегда равны 1000, а значение значения 1 часто менее 1000.

public class Test {    //value1:线程不安全    private static int value1 = 0;    //value2:使用乐观锁    private static AtomicInteger value2 = new AtomicInteger(0);    //value3:使用悲观锁    private static int value3 = 0;    private static synchronized void increaseValue3(){        value3++;    }    public static void main(String[] args) throws Exception {        //开启1000个线程,并执行自增操作        for(int i = 0; i < 1000; ++i){            new Thread(new Runnable() {                @Override                public void run() {                    try {                        Thread.sleep(100);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    value1++;                    value2.getAndIncrement();                    increaseValue3();                }            }).start();        }        //打印结果        Thread.sleep(1000);        System.out.println("线程不安全:" + value1);        System.out.println("乐观锁(AtomicInteger):" + value2);        System.out.println("悲观锁(synchronized):" + value3);    }}скопировать код

Первый, кто представил AtomicInteger. AtomicInteger — атомарный класс, предоставляемый пакетом java.util.concurrent.atomic, который использует операцию CAS, предоставляемую ЦП, для обеспечения атомарности; помимо AtomicInteger, существует множество атомарных классов, таких как AtomicBoolean, AtomicLong и AtomicReference.

Давайте взглянем на исходный код AtomicInteger, чтобы понять, как реализована его операция автоинкремента getAndIncrement() (исходный код — это Java7 в качестве примера, Java8 отличается, но идея аналогична).

public class AtomicInteger extends Number implements java.io.Serializable {    //存储整数值,volatile保证可视性    private volatile int value;    //Unsafe用于实现对底层资源的访问    private static final Unsafe unsafe = Unsafe.getUnsafe();    //valueOffset是value在内存中的偏移量    private static final long valueOffset;    //通过Unsafe获得valueOffset    static {        try {            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));        } catch (Exception ex) { throw new Error(ex); }    }    public final boolean compareAndSet(int expect, int update) {        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);    }    public final int getAndIncrement() {        for (;;) {            int current = get();            int next = current + 1;            if (compareAndSet(current, next))                return current;        }    }}скопировать код

Анализ исходного кода объясняется следующим образом:

1. Операция самоинкремента, реализованная с помощью getAndIncrement(), является спиновой CAS-операцией: compareAndSet выполняется в цикле, и в случае успешного выполнения завершается, в противном случае выполняется всегда.

2. CompareAndSet — это ядро ​​операции CAS, которая реализуется с помощью объекта Unsafe.

3. Кто небезопасен?Unsafe — это класс, используемый для помощи Java в доступе к базовым ресурсам операционной системы.(Например, память может быть выделена и освобождена), через Unsafe у Java есть базовая операционная способность, которая может повысить эффективность работы; мощная базовая операционная способность ресурсов также несет риски безопасности (название класса Unsafe также напоминает нам о это), поэтому пользователи не могут использовать его в обычных условиях. Здесь AtomicInteger использует функциональность CAS, предоставляемую Unsafe.

4.valueOffset можно понимать как смещение значения в памяти, что соответствует V в трех операндах (V/A/B) CAS, смещение также получается через Unsafe.

5. Модификатор volatile поля значения: JДля обеспечения безопасности потоков в параллельном программировании ava необходимо гарантировать атомарность, видимость и упорядоченность;Операции CAS могут гарантировать атомарность, тогда как volatile может гарантировать видимость и определенную степень упорядоченности; в AtomicInteger volatile и CAS вместе гарантируют потокобезопасность. Описание принципа действия volatile включает модель памяти Java (JMM), подробное описание которой здесь не приводится.

После разговора об AtomicInteger давайте поговорим о synchronized. Synchronized обеспечивает безопасность потоков, блокируя блок кода: одновременно только один поток может выполнять код в блоке кода.Synchronized — это тяжеловесная операция не только потому, что блокировка требует дополнительных ресурсов, но и потому, что переключение состояния потока будет включать преобразование состояния ядра операционной системы и состояния пользователя;Однако благодаря ряду оптимизаций блокировок с помощью JVM (таких как спин-блокировки, облегченные блокировки, огрубление блокировки и т. д.) производительность synchronized становилась все лучше и лучше.

2. Механизм номера версии

В дополнение к CAS для реализации оптимистической блокировки также можно использовать механизм номера версии. Основная идея механизма номера версии состоит в том, чтобы добавить к данным поле version, которое представляет номер версии данных.Всякий раз, когда данные изменяются, номер версии увеличивается на 1. Когда поток запрашивает данные, номер версии данных находится вместе; когда поток обновляет данные, оценивается, согласуется ли текущий номер версии с номером версии, прочитанным ранее, и операция выполняется только в том случае, если они согласуются. .

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

Давайте возьмем в качестве примера «обновление золотых монет игрока» (база данных — MySQL, и то же самое верно для других баз данных), чтобы увидеть, как механизмы пессимистической блокировки и номера версии справляются с проблемами параллелизма.

Рассмотрим такой сценарий: игровой системе необходимо обновить золотые монеты игрока, а обновленные золотые монеты зависят от текущего состояния (например, золотых монет, уровня и т. д.), поэтому перед обновлением необходимо запросить текущее состояние игрока.

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

@Transactionalpublic void updateCoins(Integer playerId){    //根据player_id查询玩家信息    Player player = query("select coins, level from player where player_id = {0}", playerId);    //根据玩家当前信息及其他信息,计算新的金币数    Long newCoins = ……;    //更新金币数    update("update player set coins = {0} where player_id = {1}", newCoins, playerId);}скопировать код

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

@Transactionalpublic void updateCoins(Integer playerId){    //根据player_id查询玩家信息(加排它锁)    Player player = queryForUpdate("select coins, level from player where player_id = {0} for update", playerId);    //根据玩家当前信息及其他信息,计算新的金币数    Long newCoins = ……;    //更新金币数    update("update player set coins = {0} where player_id = {1}", newCoins, playerId);}скопировать код

Механизм номера версии — это еще один способ мышления, который добавляет поле к информации об игроке: версия. Когда информация о проигрывателе запрашивается в первый раз, также запрашивается информация о версии, при выполнении операции обновления проверяется, изменилась ли версия.Если версия изменилась, обновление не будет выполнено.

@Transactionalpublic void updateCoins(Integer playerId){    //根据player_id查询玩家信息,包含version信息    Player player = query("select coins, level, version from player where player_id = {0}", playerId);    //根据玩家当前信息及其他信息,计算新的金币数    Long newCoins = ……;    //更新金币数,条件中增加对version的校验    update("update player set coins = {0} where player_id = {1} and version = {2}", newCoins, playerId, player.version);}скопировать код

3. Преимущества и недостатки и применимые сценарии

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

1. Функциональные ограничения

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

Например, CAS может гарантировать атомарность операций только с одной переменной, когда задействовано несколько переменных, CAS бессилен, а с синхронизацией можно справиться, заблокировав весь блок кода. Другим примером является механизм номера версии.Если запрос относится к таблице 1, а обновление — к таблице 2, трудно добиться оптимистической блокировки с помощью простого номера версии.

2. Интенсивность конкуренции

Если можно использовать как пессимистическую, так и оптимистическую блокировку, то выбор зависит от степени конкуренции:

Когда конкуренция не интенсивна (вероятность конфликтов параллелизма мала), оптимистическая блокировка более выгодна, поскольку пессимистическая блокировка блокирует блоки кода или данные, другие потоки не могут получить к ним доступ одновременно, что влияет на параллелизм, а блокировка и снятие блокировки требуют дополнительных ресурсов.

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

В-четвертых, интервьюер спросил: заперт ли замок оптимизма?

Когда я проходил собеседование, я столкнулся с этим вопросом от интервьюера. Вот мое понимание проблемы:

1. Оптимистическая блокировка сама по себе не блокируется, просто проверьте, были ли данные обновлены другими потоками при обновлении; например, AtomicInteger.

2. Иногда оптимистическая блокировка может взаимодействовать с операциями блокировки., например, в вышеупомянутом примере updateCoins() MySQL добавит эксклюзивную блокировку при выполнении обновления. Но это всего лишь пример взаимодействия между оптимистической блокировкой и операцией блокировки, и он не может изменить того факта, что «оптимистическая блокировка сама по себе не блокируется».

5. Интервьюер спросил: Каковы недостатки CAS?

На этом этапе собеседования вы уже можете понравиться интервьюеру. Но интервьюер готов нанести вам решающий удар: знаете ли вы недостатки этой реализации CAS?

Вот некоторые из далеко не идеальных мест в CAS:

1. Проблема АВА

Предполагая, что есть два потока — поток 1 и поток 2, два потока делают следующее по порядку:

  • (1) Поток 1 считывает данные в памяти как A;

  • (2) поток 2 изменяет данные на B;

  • (3) поток 2 изменяет данные на A;

  • (4) Поток 1 выполняет операции CAS над данными

На шаге (4), поскольку данные в памяти по-прежнему A, операция CAS завершается успешно, но на самом деле данные были изменены потоком 2. Это проблема АБА.

В случае с AtomicInteger ABA, похоже, не приносит никакого вреда. Однако в некоторых сценариях ABA несет в себе скрытые опасности, такие как проблема вершины стека: вершина стека была изменена дважды (или более) и исходное значение было восстановлено, но стек мог измениться.

Для задачи ABA более эффективным решением является введение номера версии, при каждом изменении значения в памяти номер версии равен +1, при выполнении операции CAS сравнивается не только значение в памяти, но и сравнивается номер версии.CAS может быть успешно выполнен только тогда, когда ни одна из них не изменилась. Класс AtomicStampedReference в Java использует номер версии для решения проблемы ABA.

2. Проблема накладных расходов в условиях высокой конкуренции

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

3. Функциональные ограничения

Функция CAS относительно ограничена, например, CAS может гарантировать атомарность операций только с одной переменной (или одним значением в памяти), что означает: (1) атомарность не обязательно гарантирует безопасность потока. безопасность; (2) когда задействовано несколько переменных (значений памяти), CAS бессилен.

Кроме того, реализация CAS требует поддержки процессора на аппаратном уровне, в Java обычные пользователи не могут использовать его напрямую, а могут использовать только класс atomic в пакете atomic, что ограничивает гибкость.

6. Резюме

Эта статья знакомит с основными понятиями, методами реализации (включая примеры), применимыми сценариями и возможными вопросами интервьюеров об оптимистической и пессимистической блокировке, надеясь помочь вам в вашем интервью. Наконец, я желаю вам всем удачи!

Расширенное чтение

Распределенная блокировка Redis: реализация оптимистической блокировки

Анализ и практика распределенной блокировки на основе Redis

Что интервьюер спросит о механизме блокировки Java?

Принцип реализации блокировки чтения-записи в Java

Чтобы полностью понять Нетти, этой статьи достаточно

Автор: Мифы о программировании

Источник: https://www.cnblogs.com/kismetv/p/10787228.html