Почему synchronized — плохой выбор для счетчика в высококонкурентной среде?

«Почему synchronized — плохой выбор для счетчика в высококонкурентной среде?» — вопрос из категории Java Core, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Использование synchronized для счетчика при большом количестве конкурирующих потоков приводит к высокому contention (состязанию). Потоки выстраиваются в очередь для захвата одного монитора, что вызывает:

  • Накладные расходы на переключение контекста.
  • Простои потоков в состоянии BLOCKED.
  • Резкое падение пропускной способности (throughput).

Альтернативы из пакета java.util.concurrent.atomic:

  1. AtomicLong / AtomicInteger Используют CAS (Compare-And-Swap) операции на уровне процессора, что позволяет обновлять значение без полной блокировки.

    private AtomicLong counter = new AtomicLong();
    public long increment() {
        return counter.incrementAndGet(); // Неблокирующая операция
    }
  2. LongAdder / DoubleAdder (Java 8+) Оптимизированы для сценариев с высокой записной нагрузкой. Значение разделяется на несколько ячеек, уменьшая contention. Итоговое значение вычисляется только при вызове sum().

    private LongAdder adder = new LongAdder();
    public void increment() {
        adder.increment(); // Минимизирует contention
    }
    public long get() {
        return adder.sum();
    }

Рекомендация: Для частых обновлений и редких чтений используйте LongAdder. Для частых чтений и обновлений — AtomicLong.