원자적 연산(Atomic Operation)과 CAS(Compare-And-Swap) 연산은 멀티스레드 환경에서 동시성 문제를 해결하기 위한 핵심 개념입니다. 이를 이해하면 락(lock) 없이 안전하게 공유 자원을 처리할 수 있습니다.
원자적 연산이란, 더 이상 쪼갤 수 없고, 중간에 다른 스레드가 개입할 수 없는 연산을 의미합니다. 즉, 연산이 끝날 때까지 다른 어떤 연산도 끼어들지 않으며, 결과가 일관되게 유지됩니다. 멀티스레드 환경에서, 원자적 연산은 스레드가 동시에 같은 자원을 수정할 때 발생하는 Race Condition(경쟁 상태)을 방지합니다.
자바에서는 java.util.concurrent.atomic 패키지 내에서 원자적 연산을 지원하는 여러 클래스가 제공됩니다. 이 중 AtomicInteger는 대표적인 예입니다.
AtomicInteger atomicInt = new AtomicInteger(0);
// 원자적으로 값을 1 증가시킴
atomicInt.incrementAndGet();
여기서 incrementAndGet() 메서드는 스레드 간에 동기화 문제 없이 원자적으로 값을 1 증가시킵니다. 이때, 락을 사용하지 않고도 안전하게 동작합니다.
락을 사용하여 공유 자원의 접근을 제어할 수도 있지만, 락은 잠금 해제를 기다리는 동안 스레드가 blocked 상태에 빠질 수 있으며, 그로 인해 성능이 저하될 수 있습니다.
AtomicInteger 같은 클래스는 락을 사용하지 않고도 원자적 연산을 구현할 수 있습니다. 이는 CAS(Compare-And-Swap) 연산을 사용하기 때문입니다. CAS 연산은 낮은 레벨에서 CPU의 지원을 받아 구현되며, 락보다 더 효율적일 수 있습니다.
CAS(Compare-And-Swap)는 다음과 같은 절차로 작동합니다:
이 연산은 원자적으로 수행되기 때문에, 여러 스레드가 동시에 같은 변수를 수정하려 해도 Race Condition이 발생하지 않습니다. 자바에서는 이를 compareAndSet() 메서드로 제공하고 있습니다.
AtomicInteger atomicInt = new AtomicInteger(0);
// 기대값이 0일 때, 1로 변경
boolean updated = atomicInt.compareAndSet(0, 1);
이 연산에서 중요한 점은 스레드가 여러 번 시도하면서 실패할 수 있다는 것입니다. 하지만 메모리 값을 바꾸는 도중에도 락을 걸지 않으므로 성능상 이점이 있습니다.
CAS 연산은 성능 면에서 매우 효율적이지만, 스핀 락(Spin Lock) 문제가 발생할 수 있습니다. 스핀 락이란, 스레드가 값을 업데이트할 수 있을 때까지 계속 시도하는 상황을 의미합니다.
CAS는 블로킹(Blocking) 대신 비블로킹(Non-blocking) 동기화 방식의 장점을 제공합니다. 하지만 스레드 경쟁이 심한 경우에는 오히려 성능이 저하될 수 있습니다. 이러한 상황에서는 일반적인 락이 더 나을 수 있습니다.
CAS 방식은 스레드 간 경쟁이 심해지면, 스레드가 스핀 락 상태에서 계속 자원을 얻으려고 반복적으로 시도합니다. 이때 runnable 상태에서 CPU를 점유하며, 성공할 때까지 계속해서 값을 읽고 비교한 뒤, 값이 바뀌었는지 확인하여 재시도합니다. 이런 반복적인 시도가 빈번하게 발생하면, CPU 자원이 낭비되고, 결국 성능이 저하됩니다. 즉, 경쟁이 심할수록 CAS 연산이 자주 실패하고, 그만큼 재시도 횟수가 많아져 전체 시스템의 성능이 떨어지는 것입니다.
자바에서는 AtomicInteger 외에도 여러 원자적 클래스를 제공합니다. 몇 가지 주요 클래스는 다음과 같습니다:
이 클래스들은 공통적으로 CAS를 기반으로 하여 락 없이 안전한 동기화를 제공합니다.
CAS 방식은 경쟁이 적을 때 성능이 매우 뛰어나지만, 경쟁이 치열해지면 스핀 락이 발생할 수 있기 때문에 락(lock)을 병행하는 방식이 고려될 수 있습니다. 다음과 같은 상황에서는 CAS와 락을 결합하여 사용하는 것이 적합할 수 있습니다:
이처럼 상황에 맞게 CAS와 락을 병행하면 성능과 안정성을 모두 고려할 수 있습니다.