Одним из лучших дополнений в Java 5 были атомарные операции, поддерживаемые в таких классах, как AtomicInteger, AtomicLong и т. д. Эти классы помогают вам минимизировать потребность в сложном(ненужном) многопоточном коде для некоторых базовых операций, таких как увеличение или уменьшение значения, которое является общим для нескольких потоков. Эти классы внутренне полагаются на алгоритм CAS(сравнение и обмен). В этой статье я собираюсь подробно обсудить эту концепцию.
1. Оптимистическая и пессимистическая блокировка
Традиционные механизмы блокировки, например, использование ключевого слова synchronized в Java, считаются пессимистичным методом блокировки или многопоточности. Он требует от вас сначала гарантировать, что никакой другой поток не будет вмешиваться между определенными операциями(т. е. блокировать объект), а затем только разрешить вам доступ к любому экземпляру/методу.
Это все равно, что сказать: «Пожалуйста, сначала закройте дверь, иначе придет другой мошенник и переставит ваши вещи».
Хотя вышеприведенный подход безопасен и работает, но он накладывает существенный штраф на ваше приложение с точки зрения производительности. Причина проста: ожидающие потоки не могут ничего сделать, пока они также не получат шанс и не выполнят защищенную операцию.
Существует еще один подход, который более эффективен с точки зрения производительности и по своей природе оптимистичен. В этом подходе вы продолжаете обновление, надеясь, что сможете завершить его без помех. Этот подход основан на обнаружении столкновений, чтобы определить, были ли помехи от других сторон во время обновления, в этом случае операция терпит неудачу и может быть повторена(или нет).
Оптимистичный подход подобен старой поговорке: «Проще получить прощение, чем разрешение», где «проще» здесь означает «эффективнее».
«Сравнение и обмен» — хороший пример такого оптимистичного подхода, который мы обсудим далее.
2. Алгоритм сравнения и обмена
Этот алгоритм сравнивает содержимое ячейки памяти с заданным значением и, только если они совпадают, изменяет содержимое этой ячейки памяти на заданное новое значение. Это делается как одна атомарная операция. Атомарность гарантирует, что новое значение вычисляется на основе актуальной информации; если бы значение было обновлено другим потоком в это время, запись бы не удалась. Результат операции должен указывать, выполнила ли она замену; это можно сделать либо с помощью простого логического ответа(этот вариант часто называют compare-and-set), либо путем возврата значения, считанного из ячейки памяти(а не значения, записанного в нее).
Для операции CAS существует 3 параметра:
- Ячейка памяти V, в которой необходимо заменить значение.
- Старое значение A, которое было прочитано потоком в последний раз
- Новое значение B, которое должно быть записано поверх V
CAS говорит: «Я думаю, что V должно иметь значение A; если это так, поместите туда B, в противном случае не меняйте его, но скажите мне, что я ошибался». CAS — это оптимистичный метод: он продолжает обновление в надежде на успех и может обнаружить сбой, если другой поток обновил переменную с момента ее последней проверки.
3. Пример сравнения и обмена Java
Давайте разберем весь процесс на примере. Предположим, что V — это область памяти, где хранится значение «10». Есть несколько потоков, которые хотят увеличить это значение и использовать увеличенное значение для других операций, очень практичный сценарий. Давайте разобьем всю операцию CAS на шаги:
1) Потоки 1 и 2 хотят увеличить его, они оба считывают значение и увеличивают его до 11.
В = 10, А = 0, В = 0
2) Теперь поток 1 идет первым и сравнивает V со своим последним прочитанным значением:
В = 10, А = 10, В = 11
если А = ВВ = Бещеоперация не удаласьвозврат V
Очевидно, что значение V будет перезаписано как 11, т.е. операция прошла успешно.
3) Поток 2 приходит и пытается выполнить ту же операцию, что и поток 1.
В = 11, А = 10, В = 11
если А = ВВ = Бещеоперация не удаласьвозврат V
4) В этом случае V не равно A, поэтому значение не заменяется, а возвращается текущее значение V, т.е. 11. Теперь поток 2 снова повторяет эту операцию со значениями:
В = 11, А = 11, В = 12
И на этот раз условие выполняется, и увеличенное значение 12 возвращается в поток 2.
Подводя итог, когда несколько потоков пытаются обновить одну и ту же переменную одновременно с помощью CAS, один из них выигрывает и обновляет значение переменной, а остальные проигрывают. Но проигравшие не наказываются приостановкой потока. Они могут повторить операцию или просто ничего не делать.
Вот и все, что касается этой простой, но важной концепции, связанной с атомарными операциями, поддерживаемыми в Java.