
현대 소프트웨어 개발에서 멀티스레드 환경은 필수적입니다. 하지만 여러 스레드가 공유 자원에 동시에 접근할 때 발생하는 데이터 경쟁(Race Condition) 문제는 개발자를 늘 괴롭히는 요소입니다. 흔히 synchronized 키워드를 통한 '잠금(Locking)' 방식을 떠올리지만, 이는 성능 저하의 주범이 되기도 합니다. 오늘은 이에 대한 강력한 대안인 Atomic 변수와 그 핵심 원리인 CAS(Compare-And-Swap) 연산에 대해 심도 있게 다루어 보겠습니다.
1. 왜 Atomic 변수가 필요한가? (문제의 본질)
우리가 흔히 사용하는 count++ 연산은 단일 작업처럼 보이지만, 실제 CPU 레벨에서는 세 가지 단계를 거칩니다.
- 메모리에서 값을 읽어온다 (Read)
- 값을 1 증가시킨다 (Update)
- 증가된 값을 메모리에 저장한다 (Write)
멀티스레드 환경에서 두 스레드가 동시에 이 과정을 수행하면, 서로의 업데이트를 덮어쓰는 문제가 발생합니다. 이를 원자성(Atomicity)이 깨졌다고 표현합니다.
2. CAS(Compare-And-Swap) 알고리즘이란?
Atomic 변수의 핵심은 '낙관적 락(Optimistic Lock)' 기법을 사용하는 CAS 연산입니다. CAS는 하드웨어(CPU) 수준에서 제공하는 원자적 명령어로, 값을 변경하기 전에 "내가 알고 있는 기존 값이 메모리의 현재 값과 일치하는가?"를 먼저 확인합니다.
| 비교 항목 | synchronized (Blocking) | Atomic 변수 (Non-blocking) |
|---|---|---|
| 메커니즘 | 모니터 락을 획득할 때까지 스레드 대기 | 락 없이 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 점유율이 높아질 수 있습니다.
- 단일 변수에 국한: 여러 변수를 묶어서 원자적으로 처리해야 하는 복잡한 로직에는 여전히
synchronized나ReentrantLock이 적합합니다.
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
'Language > Java' 카테고리의 다른 글
| [JAVA] 메모리 누수(Memory Leak) 사례와 해결 방안 (0) | 2026.01.22 |
|---|---|
| [JAVA] 자바 동기화의 정수 : CountDownLatch vs CyclicBarrier 완벽 비교 가이드 (0) | 2026.01.22 |
| [JAVA] JVM 메모리 구조의 심층 분석 : 효율적 자원 관리의 핵심 Runtime Data Areas (0) | 2026.01.22 |
| [JAVA] Garbage Collector(GC) 완벽 가이드 : Serial부터 ZGC까지 핵심 정리 (0) | 2026.01.22 |
| [JAVA] 나만의 문법을 창조하다 : 자바 커스텀 어노테이션 설계 및 구현 가이드 (0) | 2026.01.21 |