CAS란 무엇인가
CAS(Compare-And-Swap)는 여러 스레드가 하나의 데이터를 동시에 수정할 때, 락(lock) 없이도 안전하게 값을 변경할 수 있게 해주는 CPU 수준의 원자적 연산입니다. 이름 그대로 ‘비교’하고 ‘교체’하는 두 단계를 한 번에 처리합니다. 즉, 메모리에 있는 현재 값이 내가 예상한 값과 같으면 새로운 값으로 바꾸고, 다르면 바꾸지 않습니다. 이 모든 과정은 중간에 다른 스레드가 끼어들 수 없도록 하드웨어 차원에서 원자적으로 실행됩니다. 자바의 ‘AtomicInteger’, ‘AtomicLong’ 같은 클래스들이 내부적으로 CAS를 활용해 락 없이도 안전한 증가/감소 연산을 제공합니다.
동작 원리와 예시
CAS는 세 가지 값을 받습니다. 메모리 주소(변수의 위치), 예상 값(expected), 새로운 값(new value)입니다. 동작 방식은 다음과 같습니다.
- 현재 메모리 값이 예상 값과 같은지 확인
- 같다면 새로운 값으로 교체하고 성공(true) 반환
- 다르다면 교체하지 않고 실패(false) 반환
예를 들어 은행 창구에서 번호표를 뽑는 상황을 떠올려보세요. 여러 사람이 동시에 ‘지금 대기번호가 10번이니 11번으로 바꿔주세요’라고 요청합니다. 창구 직원은 실제 번호판이 정말 10번인지 확인한 후에만 11번으로 바꿔줍니다. 만약 다른 사람이 먼저 바꿔서 이미 11번이 되어 있다면, 요청은 거절되고 그 사람은 다시 현재 번호를 확인해서 재시도합니다. 이렇게 ‘확인 후 교체’가 한 번의 원자적 동작으로 이루어지기 때문에 두 사람이 동시에 같은 번호를 가져가는 충돌이 발생하지 않습니다.
코드로 표현하면 대략 다음과 같은 흐름입니다.
if (memory.value == expected) { memory.value = newValue; return true; } else { return false; }
실패했을 때는 보통 최신 값을 다시 읽어서 CAS를 재시도하는 루프(spin loop)를 도는 방식으로 구현됩니다.
실무에서 왜 쓰는지
락(synchronized, Mutex 등)을 사용하면 한 스레드가 임계 구역에 들어가 있는 동안 다른 스레드는 대기 상태로 블로킹됩니다. 이 과정에서 컨텍스트 스위칭 비용이 발생하고, 락을 오래 잡고 있으면 전체 성능이 크게 떨어질 수 있습니다. 반면 CAS는 락을 걸지 않고 실패하면 즉시 재시도하는 방식(lock-free)이라 스레드가 잠들지 않고 계속 CPU를 사용하며 작업을 시도합니다. 경쟁이 심하지 않은 상황에서는 CAS가 락보다 훨씬 빠르고 효율적입니다. 그래서 자바의 ‘ConcurrentHashMap’, ‘AtomicInteger’, 각종 락-프리 큐 자료구조들이 내부적으로 CAS 연산을 적극적으로 활용합니다. 다만 경쟁이 매우 치열한 환경에서는 CAS 재시도가 계속 실패하며 CPU를 낭비하는 ‘busy-wait’ 문제가 생길 수 있어, 상황에 따라 락과 CAS 중 적절한 방식을 선택하는 것이 중요합니다.