Рассмотрите счётчик завершённых операций. Надёжно ли это условие запускает действие ровно один раз при достижении порога 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("Готово");
}
}
}
Нет. LongAdder.sum() не предоставляет линейно согласованный снимок значения во время конкурентных обновлений, поэтому проверка sum() == 100 не гарантирует запуск действия ровно один раз и вообще может пропустить порог.
LongAdder подходит для статистики и счётчиков, где допустимо получить немного устаревшее значение. Для координации действий на точном пороге нужна отдельная атомарная логика.
LongAdder появился как средство уменьшить конкуренцию за одну общую ячейку счётчика. При высокой нагрузке один AtomicLong может стать точкой состязания: множество потоков постоянно конкурируют за обновление одного значения посредством CAS.
LongAdder распределяет обновления по нескольким внутренним ячейкам. Потоки чаще обновляют разные ячейки, а итоговое значение получается суммированием базовой ячейки и этих ячеек. Это повышает пропускную способность, но ценой отсутствия строгого атомарного снимка результата.
В примере инкремент и проверка порога являются разными операциями. Между ними другие потоки могут изменить счётчик, а sum() может наблюдать состояние, составленное из ячеек, обновляемых конкурентно.
Поэтому значение 100 не является надёжным событием «порог пересечён именно сейчас». Действие может не выполниться, выполниться в неподходящий момент или оказаться непригодным для гарантии единственного запуска.
Внутренне LongAdder поддерживает несколько накопителей. increment() изменяет одну из ячеек, выбранную с учётом конкуренции, а sum() последовательно складывает доступные значения. Между чтением разных ячеек другие потоки могут продолжать обновления.
Документация LongAdder прямо не обещает атомарность sum() при конкурентных обновлениях. Это принципиально отличается от операции, которая одновременно изменяет счётчик и принимает решение о пересечении порога.
Если требуется ровно один запуск при достижении порога, можно использовать линейно согласованный счётчик и отдельный флаг однократного действия:
incrementAndGet() атомарно выдаёт каждому обновлению своё значение. compareAndSet гарантирует, что только один поток изменит флаг с false на true и выполнит действие.
Такое решение хуже масштабируется для очень частых обновлений, потому что все потоки конкурируют за один AtomicLong. Если точное событие не нужно, а требуется только быстро собирать метрику, LongAdder обычно предпочтительнее; для чтения итоговой статистики часто используют sum() вне критического участка обновлений.
В системе обработки заказов нужно показывать приблизительное число обработанных сообщений на странице мониторинга. Обновление происходит из большого числа рабочих потоков, а небольшая временная неточность отображения допустима.
Вариант с AtomicLong даёт строгую согласованность каждого чтения, но при высокой конкуренции может снизить производительность из-за постоянного состязания за одну переменную. Вариант с LongAdder лучше распределяет обновления, однако значение на панели может быть не точным снимком в момент чтения.
Для мониторинга выбирают LongAdder: бизнес-логика не зависит от точного момента достижения конкретного числа, а система получает лучшую пропускную способность. Если же достижение порога должно один раз открыть выпуск партии или отправить уведомление, применяют AtomicLong вместе с атомарным флагом либо передают такую координацию специализированному компоненту с явной гарантией однократного действия.
sum() == 100 на sum() >= 100?Нет. Проверка >= 100 уменьшает вероятность пропуска порога, но не делает чтение атомарным с обновлением и не гарантирует единственный запуск. Несколько потоков могут одновременно увидеть условие истинным и выполнить действие.
AtomicLong от LongAdder?AtomicLong.incrementAndGet() возвращает линейно согласованное значение, связанное с конкретным атомарным обновлением. Это позволяет определить поток, пересёкший порог, если счётчик только увеличивается.
LongAdder оптимизирован для суммирования конкурентных обновлений, но его sum() не является атомарным снимком. Поэтому его значение нельзя использовать как единственное основание для строгой синхронизации или принятия решения, которое должно произойти ровно один раз.
LongAdder, добавив synchronized только вокруг проверки?Само по себе это не решает проблему. Если increment() выполняется вне того же монитора, синхронизированный блок не образует единой атомарной операции «увеличить и проверить».
Теоретически можно синхронизировать и обновление, и чтение одним монитором, но тогда преимущество LongAdder исчезает, а реализация усложняется. Для точного порога обычно понятнее выбрать AtomicLong и отдельно защитить однократное действие через AtomicBoolean.compareAndSet.