원자적 연산과 CAS (Compare-And-Swap) 연산

서버란·2024년 9월 19일

자바 궁금증

목록 보기
28/35

원자적 연산(Atomic Operation)과 CAS(Compare-And-Swap) 연산은 멀티스레드 환경에서 동시성 문제를 해결하기 위한 핵심 개념입니다. 이를 이해하면 락(lock) 없이 안전하게 공유 자원을 처리할 수 있습니다.

1. 원자적 연산(Atomic Operation)

원자적 연산이란, 더 이상 쪼갤 수 없고, 중간에 다른 스레드가 개입할 수 없는 연산을 의미합니다. 즉, 연산이 끝날 때까지 다른 어떤 연산도 끼어들지 않으며, 결과가 일관되게 유지됩니다. 멀티스레드 환경에서, 원자적 연산은 스레드가 동시에 같은 자원을 수정할 때 발생하는 Race Condition(경쟁 상태)을 방지합니다.

자바에서는 java.util.concurrent.atomic 패키지 내에서 원자적 연산을 지원하는 여러 클래스가 제공됩니다. 이 중 AtomicInteger는 대표적인 예입니다.

AtomicInteger atomicInt = new AtomicInteger(0);

// 원자적으로 값을 1 증가시킴
atomicInt.incrementAndGet();

여기서 incrementAndGet() 메서드는 스레드 간에 동기화 문제 없이 원자적으로 값을 1 증가시킵니다. 이때, 락을 사용하지 않고도 안전하게 동작합니다.

2. AtomicInteger vs. 락(lock)

락을 사용하여 공유 자원의 접근을 제어할 수도 있지만, 락은 잠금 해제를 기다리는 동안 스레드가 blocked 상태에 빠질 수 있으며, 그로 인해 성능이 저하될 수 있습니다.

AtomicInteger 같은 클래스는 락을 사용하지 않고도 원자적 연산을 구현할 수 있습니다. 이는 CAS(Compare-And-Swap) 연산을 사용하기 때문입니다. CAS 연산은 낮은 레벨에서 CPU의 지원을 받아 구현되며, 락보다 더 효율적일 수 있습니다.

  • 락을 사용하는 경우: 스레드가 자원을 획득할 때까지 대기해야 하며, 스레드가 블록 상태(blocked)로 전환됩니다.
  • AtomicInteger 같은 클래스: 락을 사용하지 않고 CAS 연산을 통해 자원을 안전하게 수정합니다. 스레드는 블록 상태로 전환되지 않으므로 성능 면에서 이점이 있습니다.

3. CAS(Compare-And-Swap) 연산

CAS(Compare-And-Swap)는 다음과 같은 절차로 작동합니다:

  1. 메모리 상의 특정 값을 읽습니다.
  2. 예상한 값과 실제 메모리 값을 비교합니다.
  3. 값이 일치하면, 새로운 값으로 교체(swap)합니다. 그렇지 않으면 아무 작업도 하지 않고 실패를 반환합니다.

이 연산은 원자적으로 수행되기 때문에, 여러 스레드가 동시에 같은 변수를 수정하려 해도 Race Condition이 발생하지 않습니다. 자바에서는 이를 compareAndSet() 메서드로 제공하고 있습니다.

AtomicInteger atomicInt = new AtomicInteger(0);

// 기대값이 0일 때, 1로 변경
boolean updated = atomicInt.compareAndSet(0, 1);

이 연산에서 중요한 점은 스레드가 여러 번 시도하면서 실패할 수 있다는 것입니다. 하지만 메모리 값을 바꾸는 도중에도 락을 걸지 않으므로 성능상 이점이 있습니다.

4. CAS의 단점: 스핀 락(Spin Lock)

CAS 연산은 성능 면에서 매우 효율적이지만, 스핀 락(Spin Lock) 문제가 발생할 수 있습니다. 스핀 락이란, 스레드가 값을 업데이트할 수 있을 때까지 계속 시도하는 상황을 의미합니다.

  • CAS 연산이 실패하면 스레드는 값을 다시 읽고 다시 시도합니다. 이것을 스핀 대기(Spin Waiting)라고 합니다.
  • 이때 스레드는 runnable 상태를 유지하며 반복적으로 값을 비교하고 변경을 시도하지만, 다른 스레드가 계속 값을 변경하면 성공하지 못할 수 있습니다. 즉, 다른 스레드가 락을 해제할 때까지 기다리는 대신, CPU 자원을 사용해 계속 시도하는 것입니다.
  • 이로 인해 CPU 자원을 소모하게 되며, 스레드가 성능 저하를 겪을 수 있습니다. 하지만 이는 짧은 시간 내에 락을 획득할 수 있는 경우에는 오히려 효율적인 방법입니다.

CAS는 블로킹(Blocking) 대신 비블로킹(Non-blocking) 동기화 방식의 장점을 제공합니다. 하지만 스레드 경쟁이 심한 경우에는 오히려 성능이 저하될 수 있습니다. 이러한 상황에서는 일반적인 락이 더 나을 수 있습니다.

5. CAS vs 락의 선택

  • 락을 사용하는 방식은 코드가 단순하고 이해하기 쉬우며, 복잡한 동기화가 필요한 경우 적합합니다. 하지만 성능 면에서는 잠금 대기 시간이 늘어나면 문제가 될 수 있습니다.
  • CAS 방식은 더 높은 성능을 제공할 수 있지만, 경쟁이 치열한 경우 스핀 락이 길어지면서 성능 저하가 발생할 수 있습니다. 따라서 낮은 경쟁 환경에서는 CAS 방식이 유리하지만, 경쟁이 많은 상황에서는 락이 더 나을 수 있습니다.

정리

  • 원자적 연산: 더 이상 쪼갤 수 없는 연산으로, 멀티스레드 환경에서 안전함.
  • CAS 연산: 비교 후 값이 일치하면 변경, 일치하지 않으면 변경하지 않는 원자적 연산 방식.
  • AtomicInteger: 락 없이 CAS 연산을 통해 원자적 연산을 지원하는 클래스.
  • CAS의 장단점: 락을 걸지 않고 성능을 향상시킬 수 있으나, 스핀 락 문제가 발생할 수 있음.

Q1: CAS 방식이 스레드 경쟁이 심한 환경에서 성능이 떨어지는 이유는 무엇인가요?

CAS 방식은 스레드 간 경쟁이 심해지면, 스레드가 스핀 락 상태에서 계속 자원을 얻으려고 반복적으로 시도합니다. 이때 runnable 상태에서 CPU를 점유하며, 성공할 때까지 계속해서 값을 읽고 비교한 뒤, 값이 바뀌었는지 확인하여 재시도합니다. 이런 반복적인 시도가 빈번하게 발생하면, CPU 자원이 낭비되고, 결국 성능이 저하됩니다. 즉, 경쟁이 심할수록 CAS 연산이 자주 실패하고, 그만큼 재시도 횟수가 많아져 전체 시스템의 성능이 떨어지는 것입니다.

Q2: Java에서 AtomicInteger 외에 원자적 연산을 제공하는 다른 클래스는 어떤 것이 있나요?

자바에서는 AtomicInteger 외에도 여러 원자적 클래스를 제공합니다. 몇 가지 주요 클래스는 다음과 같습니다:

  • AtomicLong: long 타입의 원자적 연산을 제공합니다.
  • AtomicBoolean: boolean 타입의 원자적 연산을 제공합니다.
  • AtomicReference: 임의의 참조형 객체에 대한 원자적 연산을 제공합니다.
  • AtomicStampedReference: 버전 정보를 추가하여 값의 변경을 추적할 수 있습니다. 주로 ABA 문제를 해결하는 데 사용됩니다.
  • AtomicMarkableReference: 객체와 함께 플래그(boolean)를 함께 원자적으로 다루는 클래스입니다.

이 클래스들은 공통적으로 CAS를 기반으로 하여 락 없이 안전한 동기화를 제공합니다.

Q3: CAS 방식과 락을 결합하여 사용하는 경우는 어떤 경우에 적합한가요?

CAS 방식은 경쟁이 적을 때 성능이 매우 뛰어나지만, 경쟁이 치열해지면 스핀 락이 발생할 수 있기 때문에 락(lock)을 병행하는 방식이 고려될 수 있습니다. 다음과 같은 상황에서는 CAS와 락을 결합하여 사용하는 것이 적합할 수 있습니다:

  • 경쟁이 낮거나 일시적으로 높은 상황: 대부분의 경우 CAS로 빠르게 처리가 가능하지만, 특정 순간에 경쟁이 높아지면 락을 사용하는 방식으로 전환할 수 있습니다. 이를 적응적 락(Adaptive Locking) 방식이라고 하며, 상황에 따라 락과 CAS를 동적으로 전환합니다.
  • 복잡한 동작을 수행할 때: CAS는 단순한 값 교체에 적합하지만, 여러 변수를 동시에 변경해야 하거나 복잡한 로직을 수행할 때는 락을 사용하는 것이 더 안전합니다.
  • 성능이 중요한 동시에 동기화 문제가 중요한 경우: 성능을 유지하면서도 안정적인 동기화가 필요하다면, CAS를 기본으로 사용하고, 필요에 따라 락을 활용하는 하이브리드 방식을 적용할 수 있습니다.

이처럼 상황에 맞게 CAS와 락을 병행하면 성능과 안정성을 모두 고려할 수 있습니다.

profile
백엔드에서 서버엔지니어가 된 사람

0개의 댓글