Статьи

Как работает CAS (сравнение и замена) в Java

Прежде чем мы углубимся в стратегию 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.

Это все, ребята. Надеюсь, вам понравилось.