Статьи

Хотите быстрее с AtomicLong?

Я часто слышу, что атомарные типы Java (java.util.concurrent.atomic) супербыстрые и прекрасно работают с высококонкурентным кодом. Большую часть времени атомщики выполняют свою работу надежно и эффективно. Однако есть сценарии, в которых скрытая стоимость неуправляемой конкуренции за атомарные типы становится серьезной проблемой производительности. Давайте посмотрим, как реализованы типы java.util.concurrent.atomic.Atomic * и что подразумевает этот дизайн.

Все атомарные типы, такие как AtomicLong, AtomicBoolean, AtomicReference и т. Д., По сути являются обертками для изменчивых значений. Дополнительную ценность дает внутреннее использование sun.misc.Unsafe, которое предоставляет возможности CAS для этих типов.

CAS (сравнение и замена) по сути является атомарной инструкцией, реализованной современным аппаратным обеспечением ЦП, которая позволяет неблокирующим, многопоточным манипулировать данными безопасным и эффективным способом. Огромным преимуществом CAS перед блокировкой является тот факт, что CAS не несет никаких накладных расходов на уровне ядра, поскольку никакого арбитража не происходит. Вместо этого компилятор выдает инструкции процессора, такие как lock cmpxchg, lock xadd, lock addq и т. Д. Это так же быстро, как вы можете получить, вызывая инструкции с точки зрения JVM.

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

Этот вопрос был рассмотрен в очень интересном исследовании Дейва Дайса, Дэнни Хендлера и Ильи Мирского. Я настоятельно рекомендую прочитать всю статью, так как она содержит гораздо больше ценной информации, чем эта короткая статья.

Я воспроизвел некоторые концепции из бумаги и проверил их. Многие Java-программисты должны находить результаты достаточно показательными, поскольку существует распространенное заблуждение относительно производительности атомарного (CAS).

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.LockSupport;
  
public class BackOffAtomicLong {
  
    public static long bk;
  
    private final AtomicLong value = new AtomicLong(0L);
  
    public long get() {
        return value.get();
    }
  
    public long incrementAndGet() {
        for (;;) {
            long current = get();
            long next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }
  
    public boolean compareAndSet(final long current, final long next) {
        if (value.compareAndSet(current, next)) {
            return true;
        } else {
            LockSupport.parkNanos(1L);
            return false;
        }
    }
  
    public void set(final long l) {
        value.set(l);
    }
  
}

Тесты были выполнены на 64-битной Linux 3.5.0 (x86_64) с процессором Intel® Core iM- i32-3632QM @ 2,20 ГГц (8 логических ядер) с использованием 64-битной Hotspot Java 1.7.0_25-b15.

Как и ожидалось, для высокой нагрузки не было существенной разницы между двумя реализациями:

bal01

Тем не менее, это становится гораздо интереснее при большом споре в магазине. Этот сценарий демонстрирует слабость оптимистического подхода к повторным попыткам, используемого реализацией AtomicLong из Hotspot.

bal02

Аналогичным образом, при смешанном конфликте между читателями и авторами также очевидны преимущества облегченного управления доступом.

bal03

Результаты существенно различаются, когда задействована связь между сокетами, но, к сожалению, я почему-то потерял результаты тестирования на оборудовании на базе Intel Xeon. Не стесняйтесь размещать результаты для разных архитектур / JVM.