CAS는 비차단 알고리즘의 대표격입니다.
expected)과 실제 메모리 값을 비교하고, 같을 때만 새로운 값으로 교체합니다. synchronized나 ReentrantLock은 차단(blocking) 방식으로, 락을 얻지 못하면 스레드를 대기 큐에 넣고 문맥 전환(context switch)이 발생합니다. 락(synchronized/Lock)을 획득·해제할 때는 JVM 내부에서
1. 스핀 혹은 OS futex(fast userspace mutex) 호출
2. 대기 스레드를 OS 스케줄러에 등록
3. 컨텍스트 전환 및 스케줄링
과 같은 일이 일어납니다.
이 과정에서 발생하는 수백~수천 사이클의 오버헤드는,
CAS의 한두 개의 기계어 명령다.
모던 멀티코어 시스템은 캐시 일관성 프로토콜(MESI, MOESI 등)을 통해 캐시라인 단위로 동기화합니다.
예를 들어 100개의 스레드가 동시에 increment()를 호출하는 상황에서,
compareAndSet()을 재시도하며 진행 java.util.concurrent.atomic 패키지의 AtomicInteger나 AtomicReference는 Unsafe API를 통해 네이티브 CAS 명령어와 직접 연동합니다. 본 벤치마크(COUNT = 100_000_000) 환경에서 측정된 실행 시간:
| 구현체 | 걸린 시간(ms) |
|---|---|
| BasicInteger | 8 |
| VolatileInteger | 327 |
| SyncInteger | 765 |
| MyAtomicInteger | 342 |
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));
}
}