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