Прежде чем мы углубимся в стратегию CAS (Compare And Swap) и то, как она используется атомарными конструкциями, такими как AtomicInteger , сначала рассмотрим этот код:
public class MyApp { private volatile int count = 0; public void upateVisitors() { ++count; //increment the visitors count } }
Этот пример кода отслеживает количество посетителей приложения. Что-то не так с этим кодом? Что произойдет, если несколько потоков попытаются обновить счетчик? На самом деле проблема заключается в простой маркировке количества, поскольку volatile не гарантирует атомарность, а число ++ не является атомарной операцией. Чтобы узнать больше, проверьте это .
Можем ли мы решить эту проблему, если пометим сам метод синхронизированным, как показано ниже:
public class MyApp { private int count = 0; public synchronized void upateVisitors() { ++count; //increment the visitors count } }
Будет ли это работать? Если да, то какие изменения мы сделали на самом деле?
Этот код гарантирует атомарность? Да.
Этот код гарантирует видимость? Да.
Тогда в чем проблема?
Он использует блокировку, что приводит к большим задержкам и накладным расходам. Проверьте эту статью . Это очень дорогой способ заставить вещи работать.
Чтобы преодолеть эти проблемы, были введены атомные конструкции. Если мы используем AtomicInteger для отслеживания количества, он будет работать.
public class MyApp { private AtomicInteger count = new AtomicInteger(0); public void upateVisitors() { count.incrementAndGet(); //increment the visitors count } }
Классы, которые поддерживают атомарные операции, например AtomicInteger, AtomicLong и т. Д., Используют CAS. CAS не использует блокировку, скорее это очень оптимистичный характер. Это следует за этими шагами:
- Сравните значение примитива со значением, которое мы получили.
- Если значения не совпадают, это означает, что какой-то поток между ними изменил значение. Иначе он пойдет дальше и поменяет значение на новое.
Проверьте следующий код в классе AtomicLong:
public final long incrementAndGet() { for (;;) { long current = get(); long next = current + 1; if (compareAndSet(current, next)) return next; } }
В JDK 8 приведенный выше код был изменен на единый внутренний:
public final long incrementAndGet() { return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L; }
Какое преимущество имеет этот единственный внутренний элемент?
На самом деле эта строка является встроенной в JVM, которая преобразуется JIT в оптимизированную последовательность команд. В случае архитектуры x86 это всего лишь одна инструкция процессора LOCK XADD, которая может дать лучшую производительность, чем классическая загрузка CAS .
Теперь подумайте о возможности, когда у нас высокий уровень конкуренции и несколько потоков хотят обновить одну и ту же атомарную переменную. В этом случае существует вероятность того, что блокировка превзойдет атомарные переменные, но на реалистичных уровнях конкуренции атомные переменные превзойдут блокировку. В Java 8 появилась еще одна конструкция LongAdder . Согласно документации:
Этот класс обычно предпочтительнее AtomicLong, когда несколько потоков обновляют общую сумму, которая используется для таких целей, как сбор статистики, а не для детального управления синхронизацией. В условиях низкой конкуренции за обновление оба класса имеют сходные характеристики. Но в условиях высокой конкуренции ожидаемая пропускная способность этого класса значительно выше за счет более высокого потребления пространства.
Так что LongAdder не всегда является заменой AtomicLong. Нам необходимо рассмотреть следующие аспекты:
- Когда нет разногласий, AtomicLong работает лучше.
- LongAdder выделит ячейки (последний класс, объявленный в абстрактном классе Striped64 ), чтобы избежать конфликтов, которые занимают память. Поэтому, если у нас ограниченный бюджет памяти, мы должны предпочесть AtomicLong.
Это все, ребята. Надеюсь, вам понравилось.