멀티쓰레드 - CAS , LOCK, VOLATILE 성능테스트

희운·2025년 7월 24일

CAS(Compare-And-Swap) vs. Lock: 성능 차이의 근본 원인

1. 비차단(Non‑Blocking) 알고리즘

CAS는 비차단 알고리즘의 대표격입니다.

  • 스레드는 공유 변수의 예상 값(expected)과 실제 메모리 값을 비교하고, 같을 때만 새로운 값으로 교체합니다.
  • 다른 스레드가 변수 값을 바꾸고 있더라도 해당 스레드는 대기(block)하지 않고 즉시 재시도(retry)할 뿐입니다.
  • 반면 synchronizedReentrantLock차단(blocking) 방식으로, 락을 얻지 못하면 스레드를 대기 큐에 넣고 문맥 전환(context switch)이 발생합니다.

2. 커널 모드 전환 및 스케줄링 오버헤드 제거

락(synchronized/Lock)을 획득·해제할 때는 JVM 내부에서
1. 스핀 혹은 OS futex(fast userspace mutex) 호출
2. 대기 스레드를 OS 스케줄러에 등록
3. 컨텍스트 전환 및 스케줄링
과 같은 일이 일어납니다.
이 과정에서 발생하는 수백~수천 사이클의 오버헤드는,
CAS의 한두 개의 기계어 명령다.

3. 캐시 일관성 프로토콜 최적화

모던 멀티코어 시스템은 캐시 일관성 프로토콜(MESI, MOESI 등)을 통해 캐시라인 단위로 동기화합니다.

  • CAS는 한 캐시라인에서 해당 필드만 교체하며, 다른 라인에는 영향을 주지 않습니다.
  • 락 해제/획득 시 JVM과 OS는 관련 캐시라인을 플러시(flush) 또는 무효화(invalidate)해야 하는데,
    이는 불필요한 메모리 대역폭 및 추가 지연을 초래합니다.

4. 스케일 아웃(Scale-Out) 시 컨텐션(Contention) 감소

  • 낮은 컨텐션: 스레드가 짧은 주기로 재시도만 하기 때문에, 다른 스레드와 경쟁이 발생해도 전체 처리량이 급격히 떨어지지 않습니다.
  • 높은 컨텐션: 락의 경우 한 번 락 보유자가 길게 작업 중이면, 다른 모든 스레드는 대기상태로 빠지며 CPU 자원을 활용하지 못합니다.

예를 들어 100개의 스레드가 동시에 increment()를 호출하는 상황에서,

  • CAS는 각 스레드가 독립적으로 compareAndSet()을 재시도하며 진행
  • 락은 1개의 스레드만 진입 후 나머지는 전부 블록 → CPU 유휴 증가

5. JVM과 하드웨어의 협업

  • java.util.concurrent.atomic 패키지의 AtomicIntegerAtomicReferenceUnsafe API를 통해 네이티브 CAS 명령어와 직접 연동합니다.
  • JVM은 메모리 배리어(memory barrier)를 적절히 삽입해, 메모리 가시성을 확보하면서도 불필요한 배리어 비용은 최소화합니다.

결론 및 실제 벤치마크 결과

본 벤치마크(COUNT = 100_000_000) 환경에서 측정된 실행 시간:

구현체걸린 시간(ms)
BasicInteger8
VolatileInteger327
SyncInteger765
MyAtomicInteger342
  • BasicInteger(락/동기화 없이 순수 연산): 최상의 성능
  • MyAtomicInteger(CAS): 락 기반 방식보다 약 2배 빠른 처리
  • SyncInteger/ReentrantLock(락): 커널 전환과 스케줄링 오버헤드로 크게 느림
  • VolatileInteger(volatile만): 메모리 배리어 비용으로 CAS보다 빠르지만 쓰기 비용 증가

CAS 연산이 빠른 이유는 “작고 단일한 하드웨어 원자 명령” + “락을 얻기 위한 시스템 콜 불필요” + “캐시 일관성만 최소한으로 유지”하기 때문입니다.
락 기반 동기화가 필요한 복잡한 임계영역이 아니라, 단일 변수 증감처럼 간단한 동시성 제어에는 CAS가 훨씬 더 적합합니다.


아래 코드는 실제 테스트해본 코드입니다.

package thread.cas.increment;


public class IncrementPerformanceMain {

    public static final long COUNT = 100_000_000;

    public static void main(String[] args) {

        test(new BasicInteger());
        test(new VolatileInteger());
        test(new SyncInteger());
        test(new MyAtomicInteger());

    }

    private static void test(IncrementInteger incrementInteger) {
        long startMs = System.currentTimeMillis();

        for (int i = 0; i < COUNT; i++) {
            incrementInteger.increment();
        }

        long endMs = System.currentTimeMillis();

        System.out.println(incrementInteger.getClass().getSimpleName() + ": ms=" + (endMs - startMs));

    }
}


profile
기록하는 공간

0개의 댓글