JavaRush /Курсы /JSP & Servlets /Атомарные операции в Java

Атомарные операции в Java

JSP & Servlets
19 уровень , 1 лекция
Открыта

Предпосылки появления атомарных операций

Давай разберем данный пример, который поможет понять работу атомарных операций:


public class Counter {
    int count; 
 
    public void increment() {
        count++;
    }
}

Когда у нас один поток, все работает классно, но если мы добавляем многопоточку, то получаем неправильные результаты, а все из-за того, что операция инкремента составляет не одну операцию, а три: запрос на получение текущего значения count, потом увелечение ее на 1 и запись снова в count.

И когда два потока захотят увеличить переменную, скорее всего, ты потеряешь данные. То есть оба потока получают 100, в результате оба запишут 101 вместо ожидаемого значения 102.

И как же это решить? Нужно использовать блокировки. Ключевое слово synchronized помогает решить данную проблему, использование этого слова дает вам гарантию, что один поток будет обращаться к методу единовременно.


public class SynchronizedCounterWithLock {
    private volatile int count;
 
    public synchronized void increment() {
        count++;
    }
}

Плюс надо добавлять ключевое слово volatile, которое обеспечивает корректную видимость ссылок среди потоков. Мы разбирали его работу выше.

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

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

Атомарные операции

Алгоритм использует низкоуровневые машинные инструкции, такие как сравнение и замена (CAS, compare-and-swap, что обеспечивает целостность данных и по ним уже существует большое количество исследований).

Типичная операция CAS работает с тремя операндами:

  • Место в памяти для работы (M)
  • Существующее ожидаемое значение (A) переменной
  • Новое значение (B), которое необходимо установить

CAS атомарно обновляет M до B, но только если значение M совпадает с A, в противном случае никаких действий предприниматься не будет.

В первом и втором случае вернут значение М. Это позволяет объединить три шага, а именно — получение значения, сравнение значения и его обновление. И это все превращается в одну операцию на машинном уровне.

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

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

Знакомство с атомарными типами

Ты столкнулся с ситуацией, когда тебе нужно настроить синхронизацию для самой простой переменной типа int?

Первый способ, который мы уже разобрали – это использование volatile + synchronized. Но есть еще специальные классы Atomic*.

Если у нас используется CAS, то операции работают быстрее по сравнению с первым способом. И в дополнение у нас есть специальные и очень удобные методы для добавления значения и операции инкремента и декремента.

AtomicBoolean, AtomicInteger, AtomicLong, AtomicIntegerArray, AtomicLongArray —классы в которых операции атомарны. Ниже мы разберем работу с ними.

AtomicInteger

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

У него есть методы get и set, которые работают, как чтение и запись по переменным.

То есть “происходит до (happens-before)” с любым последующим получением той же переменной, о которой мы говорили ранее. У атомарного метода compareAndSet также есть эти особенности согласованности памяти.

Все операции, которые возвращают новое значение, выполняются атомарно:

int addAndGet (int delta) Добавляет определенное значение к текущему значению.
boolean compareAndSet (ожидаемое int, обновление int) Устанавливает значение для данного обновленного значения, если текущее значение совпадает с ожидаемым значением.
int decrementAndGet () Уменьшает на единицу текущее значение.
int getAndAdd (int delta) Добавляет данное значение к текущему значению.
int getAndDecrement () Уменьшает на единицу текущее значение.
int getAndIncrement () Увеличивает на единицу текущее значение.
int getAndSet (int newValue) Устанавливает заданное значение и возвращает старое значение.
int incrementAndGet () Увеличивает на единицу текущее значение.
lazySet (int newValue) В конце-концов устанавливается на заданное значение.
boolean weakCompareAndSet (ожидаемое, обновление int) Устанавливает значение для данного обновленного значения, если текущее значение совпадает с ожидаемым значением.

Пример:


ExecutorService executor = Executors.newFixedThreadPool(5);
IntStream.range(0, 50).forEach(i -> executor.submit(atomicInteger::incrementAndGet));
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS);

System.out.println(atomicInteger.get()); // выведет 50
Комментарии (16)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Николай Уровень 22
31 октября 2023
"Устанавливает значение для данного обновленного значения, если текущее значение совпадает с ожидаемым значением." В меморис.
Николай Уровень 22
31 октября 2023
а зачем нужен volaile если при выходе из синхронизированного метода и сбрасываются значения в основную память, а при заходе - читаются из основной памяти?
Ezh Уровень 41
14 октября 2024
правильно, он там не нужен
Aseevim2015@gmail.com Уровень 37
19 октября 2023
Алгоритм использует низкоуровневые машинные инструкции, такие как сравнение и замена (CAS, compare-and-swap, что обеспечивает целостность данных и по ним уже существует большое количество исследований). Типичная операция CAS работает с тремя операндами: Место в памяти для работы (M) Существующее ожидаемое значение (A) переменной Новое значение (B), которое необходимо установить CAS атомарно обновляет M до B, но только если значение M совпадает с A, в противном случае никаких действий предприниматься не будет. В первом и втором случае вернут значение М. - Что это? Какие случаи? Это письмо из Простоквашино дяде Федору?))
Михаил Шапошников Уровень 111 Expert
26 октября 2023
**Если два потока одновременно обратятся к атомарной переменной**, например, чтобы инкрементировать её, внутренний механизм будет использовать так называемую **"сравни и установи"** **(CAS, compare-and-swap)** операцию. ### Как это работает? 1. Каждый поток считывает текущее значение переменной. 2. Поток выполняет вычисление (например, инкремент). 3. Поток пытается установить новое значение, но только если текущее значение не изменилось с момента последнего чтения. Это делается с использованием CAS. 4. Если значение изменилось (другой поток успел первым), операция CAS не удастся, и поток повторит попытку, начав с первого шага. **Этот цикл повторяется до тех пор, пока CAS не завершится успешно.** Этот подход называется "спинлоком" (spin-locking), потому что поток "крутится" в цикле, пока не сможет успешно выполнить операцию. Важно отметить, что **такой подход эффективен при небольшом количестве конфликтов, когда CAS обычно завершается успешно с первой или второй попытки.** Но если есть много конфликтов (многие потоки постоянно конкурируют за доступ к переменной), это может стать узким местом в производительности.
Igor Уровень 1 Expert
17 августа 2023
В примере нет строки где создают и инициализируют объект AtomicInteger с именем atomicInteger и значением 0. Поэтому так сложно читать код, особенно кто еще не очень хорошо понимает лямбды и method reference.
Надежда Уровень 104 Expert
3 августа 2023
Последняя задача. Вот output (один из возможных), если применить готовое решение автора задачи. Чтобы выполнялось требование и вывод на экран заканчивался фразой "Did someone hit? true", предложу вот такое решение (дать потоку, который первым "стреляет", немного времени, чтобы он полностью отработал блок кода и поменял значение флага на true. Трех миллисекунд на это должно хватить, и последний зашедший поток "увидит", что флаг true и выведет в консоль):
Gregory Parfer Уровень 82 Expert
20 февраля 2023
Последняя задача не проходит валидацию, пишет что "Вывод на экран должен заканчиваться фразой "Did someone hit? true"", при подстановке "правильного решения" все то же самое
FatCat Уровень 51
11 февраля 2023
прекрасно
partiec Уровень 33
20 января 2023
на статью не тяне
Сергей Уровень 84 Expert
23 октября 2022
@Mentor-02 Может и правда пофиксите последний пример, он же не работает.
Саша И. Уровень 101 Expert
12 января 2023
По состоянию на январь - работает :)
Pavel Soros Уровень 34
6 октября 2022
А вы последний пример запускали??? 250 он не выведет никогда!!! Вывод будет 50... и то если дать поспать главному потоку.
Khabibullaev Уровень 38
14 ноября 2023
Он и не должен выводить 250. Должен - 50.