본문 바로가기
카테고리 없음

[JAVA] Atomic 변수와 CAS 알고리즘 : 멀티스레드 환경의 성능 혁신

by Papa Martino V 2026. 1. 21.
728x90
Atomic 변수와 CAS 알고리즘

 

현대 소프트웨어 개발에서 멀티스레드 환경은 필수적입니다. 하지만 여러 스레드가 공유 자원에 동시에 접근할 때 발생하는 데이터 경쟁(Race Condition) 문제는 개발자를 늘 괴롭히는 요소입니다. 흔히 synchronized 키워드를 통한 '잠금(Locking)' 방식을 떠올리지만, 이는 성능 저하의 주범이 되기도 합니다. 오늘은 이에 대한 강력한 대안인 Atomic 변수와 그 핵심 원리인 CAS(Compare-And-Swap) 연산에 대해 심도 있게 다루어 보겠습니다.


1. 왜 Atomic 변수가 필요한가? (문제의 본질)

우리가 흔히 사용하는 count++ 연산은 단일 작업처럼 보이지만, 실제 CPU 레벨에서는 세 가지 단계를 거칩니다.

  1. 메모리에서 값을 읽어온다 (Read)
  2. 값을 1 증가시킨다 (Update)
  3. 증가된 값을 메모리에 저장한다 (Write)

멀티스레드 환경에서 두 스레드가 동시에 이 과정을 수행하면, 서로의 업데이트를 덮어쓰는 문제가 발생합니다. 이를 원자성(Atomicity)이 깨졌다고 표현합니다.


2. CAS(Compare-And-Swap) 알고리즘이란?

Atomic 변수의 핵심은 '낙관적 락(Optimistic Lock)' 기법을 사용하는 CAS 연산입니다. CAS는 하드웨어(CPU) 수준에서 제공하는 원자적 명령어로, 값을 변경하기 전에 "내가 알고 있는 기존 값이 메모리의 현재 값과 일치하는가?"를 먼저 확인합니다.

메커니즘모니터 락을 획득할 때까지 스레드 대기락 없이 CAS 연산을 통해 무한 재시도(Loop)
성능컨텍스트 스위칭 비용으로 인해 무거움대기 시간이 없어 성능상 이점 (특히 가벼운 연산 시)
데이터 일관성상호 배제를 통해 보장원자적 명령어(CPU 레벨)를 통해 보장
사용 편의성코드 블록을 감싸는 형태로 직관적임객체 메서드 호출 방식으로 구현됨

3. 실무 예제: AtomicInteger 활용

실제 자바 코드에서 AtomicInteger를 사용하여 안전하게 숫자를 증가시키는 예제를 살펴보겠습니다.


import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounterExample {
    private AtomicInteger counter = new AtomicInteger(0);

    public void increment() {
        // 내부적으로 CAS 연산을 수행하여 원자성 보장
        int newValue = counter.incrementAndGet();
        System.out.println("현재 카운트: " + newValue);
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicCounterExample example = new AtomicCounterExample();
        
        // 여러 스레드가 동시에 증가 연산 수행 시뮬레이션
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for(int j=0; j<1000; j++) example.increment();
            }).start();
        }
    }
}
    

4. Atomic 변수의 한계와 주의점

장점만 있는 것은 아닙니다. 다음과 같은 현상을 인지하고 있어야 합니다.

  • ABA 문제: 값이 A였다가 B로 바뀌고 다시 A로 돌아왔을 때, CAS는 값이 변하지 않았다고 판단할 수 있습니다. 이를 해결하기 위해 AtomicStampedReference(버전 관리)를 사용합니다.
  • Busy Wait: 충돌이 잦은 환경에서는 스레드가 값을 변경하지 못하고 계속해서 루프를 돌기 때문에 CPU 점유율이 높아질 수 있습니다.
  • 단일 변수에 국한: 여러 변수를 묶어서 원자적으로 처리해야 하는 복잡한 로직에는 여전히 synchronizedReentrantLock이 적합합니다.

5. 결론: 언제 무엇을 쓸 것인가?

단순한 카운터, 플래그 변경, 통계 합산 등 가벼운 연산에는 java.util.concurrent.atomic 패키지의 변수들을 사용하는 것이 성능상 압도적으로 유리합니다. 하지만 복잡한 비즈니스 로직과 긴 임계 구역(Critical Section)이 포함된 경우에는 전통적인 락 기반의 동기화를 고려해야 합니다. 시스템의 특성을 파악하여 '병목 없는 동시성'을 설계하는 것이 전문 개발자의 역량입니다.

 
내용 출처 및 참고 문헌:

  • Java Concurrency in Practice (Brian Goetz)
  • Oracle Java Documentation: Package java.util.concurrent.atomic
  • Baeldung: Guide to Atomic Variables in Java
728x90