Рассмотрите счётчик завершённых операций. Надёжно ли это условие запускает действие ровно один раз при дост...

Рассмотрите счётчик завершённых операций. Надёжно ли это условие запускает действие ровно один раз при достижении порога 100?

import java.util.concurrent.atomic.LongAdder;

class Progress {
    private final LongAdder completed = new LongAdder();

    void mark() {
        completed.increment();
        if (completed.sum() == 100) {
            System.out.println("Готово");
        }
    }
}
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Нет. LongAdder.sum() не предоставляет линейно согласованный снимок значения во время конкурентных обновлений, поэтому проверка sum() == 100 не гарантирует запуск действия ровно один раз и вообще может пропустить порог.

LongAdder подходит для статистики и счётчиков, где допустимо получить немного устаревшее значение. Для координации действий на точном пороге нужна отдельная атомарная логика.

Исторический контекст

LongAdder появился как средство уменьшить конкуренцию за одну общую ячейку счётчика. При высокой нагрузке один AtomicLong может стать точкой состязания: множество потоков постоянно конкурируют за обновление одного значения посредством CAS.

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

Постановка проблемы

В примере инкремент и проверка порога являются разными операциями. Между ними другие потоки могут изменить счётчик, а sum() может наблюдать состояние, составленное из ячеек, обновляемых конкурентно.

Поэтому значение 100 не является надёжным событием «порог пересечён именно сейчас». Действие может не выполниться, выполниться в неподходящий момент или оказаться непригодным для гарантии единственного запуска.

Подробное решение

Внутренне LongAdder поддерживает несколько накопителей. increment() изменяет одну из ячеек, выбранную с учётом конкуренции, а sum() последовательно складывает доступные значения. Между чтением разных ячеек другие потоки могут продолжать обновления.

Документация LongAdder прямо не обещает атомарность sum() при конкурентных обновлениях. Это принципиально отличается от операции, которая одновременно изменяет счётчик и принимает решение о пересечении порога.

Если требуется ровно один запуск при достижении порога, можно использовать линейно согласованный счётчик и отдельный флаг однократного действия:

import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; class Progress { private final AtomicLong completed = new AtomicLong(); private final AtomicBoolean reported = new AtomicBoolean(); void mark() { long value = completed.incrementAndGet(); if (value >= 100 && reported.compareAndSet(false, true)) { System.out.println("Готово"); } } }

incrementAndGet() атомарно выдаёт каждому обновлению своё значение. compareAndSet гарантирует, что только один поток изменит флаг с false на true и выполнит действие.

Такое решение хуже масштабируется для очень частых обновлений, потому что все потоки конкурируют за один AtomicLong. Если точное событие не нужно, а требуется только быстро собирать метрику, LongAdder обычно предпочтительнее; для чтения итоговой статистики часто используют sum() вне критического участка обновлений.

Ситуация из практики

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

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

Для мониторинга выбирают LongAdder: бизнес-логика не зависит от точного момента достижения конкретного числа, а система получает лучшую пропускную способность. Если же достижение порога должно один раз открыть выпуск партии или отправить уведомление, применяют AtomicLong вместе с атомарным флагом либо передают такую координацию специализированному компоненту с явной гарантией однократного действия.

Что кандидаты часто упускают

  1. Достаточно ли заменить sum() == 100 на sum() >= 100?

Нет. Проверка >= 100 уменьшает вероятность пропуска порога, но не делает чтение атомарным с обновлением и не гарантирует единственный запуск. Несколько потоков могут одновременно увидеть условие истинным и выполнить действие.

  1. Чем в этой ситуации отличается AtomicLong от LongAdder?

AtomicLong.incrementAndGet() возвращает линейно согласованное значение, связанное с конкретным атомарным обновлением. Это позволяет определить поток, пересёкший порог, если счётчик только увеличивается.

LongAdder оптимизирован для суммирования конкурентных обновлений, но его sum() не является атомарным снимком. Поэтому его значение нельзя использовать как единственное основание для строгой синхронизации или принятия решения, которое должно произойти ровно один раз.

  1. Можно ли оставить LongAdder, добавив synchronized только вокруг проверки?

Само по себе это не решает проблему. Если increment() выполняется вне того же монитора, синхронизированный блок не образует единой атомарной операции «увеличить и проверить».

Теоретически можно синхронизировать и обновление, и чтение одним монитором, но тогда преимущество LongAdder исчезает, а реализация усложняется. Для точного порога обычно понятнее выбрать AtomicLong и отдельно защитить однократное действие через AtomicBoolean.compareAndSet.