본문으로 건너뛰기
김신건의 로그

[Java] LongAdder

· 수정 · 📖 약 1분 · 380자/단어 #java #concurrent #atomic #counter #striping #contention
LongAdder, java.util.concurrent.atomic.LongAdder, DoubleAdder, 스트라이프 카운터

정의

java.util.concurrent.atomic.LongAdder고경합 카운터 를 위한 자료구조. JDK 1.8 도입. AtomicLong 보다 동시 증가 throughput 이 압도적으로 높다.

원리는 striped counter (분산 누적). 여러 cell 에 나누어 누적하고, 합산할 때 모두 더한다. CAS 충돌이 발생하면 다른 cell 을 시도 → 거의 모든 increment 가 무경합.

시각화

동작

LongAdder count = new LongAdder();

count.increment();         // ++count
count.add(5);
count.sum();               // 모든 cell 합산, 추정값
count.longValue();         // == sum()
count.reset();

AtomicLong.incrementAndGet() 처럼 새 값을 반환하지는 않는다. 누적 결과만 보장.

왜 AtomicLong 보다 빠른가

AtomicInteger / AtomicLong 의 CAS 루프는 다음과 같다.

스레드 N 개가 동시에 increment:
  T1: CAS(0, 1) 성공
  T2: CAS(0, 1) 실패 → 재시도 → CAS(1, 2) 성공
  T3: CAS(0, 1) 실패 → 재시도 → CAS(1, 2) 실패 → 재시도 → CAS(2, 3) 성공
  ...

경합이 심하면 거의 모든 스레드가 여러 번 CAS 실패. 성능이 락보다 떨어질 수 있다.

LongAdder 는.

스레드 N 개가 동시에 increment:
  T1 → cell[hash(T1) % cells.length].add(1)
  T2 → cell[hash(T2) % cells.length].add(1)
  T3 → cell[hash(T3) % cells.length].add(1)
  ... (각자 다른 cell 에 누적, 충돌 거의 없음)

cells 배열의 크기가 동적으로 늘어나면서 경합이 분산된다.

sum 의 약점

long total = adder.sum();   // 모든 cell 의 합

합산 시점에 다른 스레드가 add 중이면 그 값은 반영 안 될 수 있다. “순간 정확한 카운트” 는 보장 안 됨.

워크로드 통계, request count 같이 eventual consistency 가 허용되는 카운터에 적합.

사용 예

request 카운터

class Metrics {
    private final LongAdder requests = new LongAdder();
    private final LongAdder errors = new LongAdder();

    public void onRequest() { requests.increment(); }
    public void onError() { errors.increment(); }

    public long requestCount() { return requests.sum(); }
    public long errorCount() { return errors.sum(); }
}

Map 의 값으로

ConcurrentMap<String, LongAdder> counts = new ConcurrentHashMap<>();
counts.computeIfAbsent(key, k -> new LongAdder()).increment();

이 패턴이 흔한 idiom. ConcurrentHashMap + LongAdder 조합으로 키별 카운트.

DoubleAdder, LongAccumulator, DoubleAccumulator

  • DoubleAdder: 같은 패턴의 double 버전
  • LongAccumulator: 일반화, 임의의 람다 (prev, x) -> next 로 누적
  • DoubleAccumulator: double 버전
LongAccumulator max = new LongAccumulator(Long::max, Long.MIN_VALUE);
max.accumulate(42);
max.accumulate(100);
max.get();   // 100

AtomicLong vs LongAdder 의 선택

시나리오권장
단일 스레드 / 저경합AtomicInteger / AtomicLong
고경합 + sum 만 필요LongAdder
고경합 + 매 갱신마다 결과 필요AtomicLong (CAS 비용 감수)
custom accumulationLongAccumulator

참고

이 글의 용어 (3개)
[Java] AtomicIntegerjava
정의 는 lock-free 로 원자성을 보장 하는 래퍼. 내부적으로 CAS (Compare-And-Swap) 를 사용해 락 없이 atomic 한 read-modify-write …
[Java] ConcurrentHashMapjava
정의 는 고동시성 환경에서 사용 가능한 구현. JSR-166 (Java 5) 도입, Java 8 에서 내부 구조가 크게 재작성됐다. 과 같은 인터페이스를 제공하면서 thread-…
Non-Blocking (논블로킹)concurrency
정의 Non-Blocking 은 호출이 결과를 기다리지 않고 즉시 반환 하는 실행 방식. 결과가 준비되지 않았다면 "아직 안 됐다" 라는 신호 ( , , , 등) 를 반환하고, …

이 개념을 다룬 위키 페이지 (1)

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기