멀티스레드 환경에서의 경쟁 상태와 해결 방안

양성준·2025년 7월 13일

스프링

목록 보기
47/49

경쟁 상태(Race Condition)란?

  • 경쟁 상태(Race Condition)는 멀티스레드 환경에서 두 개 이상의 스레드가 동시에 공유 자원(메모리, 변수 등)에 접근하거나 수정할 때, 실행 순서에 따라 프로그램의 결과가 달라지는 현상을 의미한다.
  • 예를 들어, 여러 스레드가 동시에 하나의 카운터 변수를 증가시키거나 감소시키는 경우, 예상하지 못한 결과(값의 손실, 데이터 오염 등)가 발생할 수 있다.
// 동기화 없이 여러 스레드가 공유 변수 c를 증가/감소
public class Counter implements Runnable {
    private int c = 0;
    public void increment() { c++; }
    public void decrement() { c--; }
    public int getValue() { return c; }
    public void run() {
        increment();
        decrement();
    }
}
  • 위 코드에서 여러 스레드가 동시에 c에 접근하면, 결과가 예측과 다르게 나올 수 있다.

경쟁 상태의 주요 원인

  • 공유 자원에 대한 동시 접근: 여러 스레드가 동시에 같은 데이터를 읽거나 쓸 때
  • 임계 구역(Critical Section) 미보호: 공유 데이터에 접근하는 코드 블록이 동기화되지 않을 때
  • 실행 순서의 불확정성: 스케줄러에 의해 스레드 실행 순서가 매번 달라질 수 있음

경쟁 상태 해결 전략

1. 상호 배제(Mutual Exclusion)

  • 공유 자원에 동시에 접근하지 못하도록 막는 방법.

1) 뮤텍스 (Mutex)

  • Mutual Exclusion의 줄임말.
  • 하나의 스레드/프로세스만 자원에 접근할 수 있도록 하는 락.
  • 특징
    • Binary Lock (0 또는 1)
      → 한 번에 딱 하나의 스레드만 임계 구역에 들어갈 수 있음.
    • 락을 획득하면(lock), 다른 스레드는 대기해야 하고, 락을 해제(unlock)해야 다른 스레드가 들어갈 수 있음.
    • 보통 OS나 언어가 제공하는 synchronized, ReentrantLock 등이 뮤텍스 역할을 함.
  • 장점 & 단점
    • 간단하고 안전한 상호 배제
    • 교착 상태(Deadlock) 가능성 있음
    • 효율이 낮아질 수 있음 (lock → unlock 비용, 문맥 전환 비용)

2) 세마포어 (Semaphore)

  • 뮤텍스를 일반화한 것.
  • 임계 구역에 동시에 N개의 스레드가 들어갈 수 있도록 하는 카운터 기반 도구.
  • 특징
    • 카운터를 가지고 있고, 초기값은 동시에 허용할 스레드 수.
      값 > 0 → 진입 가능하고 값 감소
      값 == 0 → 대기
    • 락을 해제하면 값 증가
  • Binary Semaphore (1)는 사실상 뮤텍스와 같음.
  • Counting Semaphore (>1)는 한정된 자원을 관리하기에 좋음.
    예) DB 커넥션 풀, 주차장 입구 등
  • 장점 & 단점
    • 여러 스레드가 동시에 접근 가능 → 유연함
    • 자원의 개수를 표현 가능
    • 프로그래밍하기가 더 어렵고 실수하기 쉽다
    • 남용하면 보호가 제대로 안 됨

3) 모니터 (Monitor)

  • 뮤텍스 + 조건 변수 + 임계 구역을 하나의 객체에 캡슐화한 것.
  • 스레드 동기화를 위해 더 고수준의 도구.
  • 특징
    • 언어나 시스템이 제공하는 고수준 동기화 메커니즘.
    • 객체 내부에 락과 대기/알림 메커니즘이 숨겨져 있어 사용이 간편.
    • Java에서는 모든 객체가 모니터를 가지고 있고, synchronized로 진입.
    • 모니터는 보통 하나의 스레드만 임계 구역에 들어갈 수 있으며, wait(), notify() 등으로 상태를 기다리거나 깨움.
  • 장점 & 단점
    • 락과 조건변수가 캡슐화 → 사용이 쉽고 안전
    • 코드가 깔끔하고 관리가 쉬움
    • 내부 구조가 숨겨져 있어 유연성이 떨어짐
    • 낮은 수준의 제어가 어렵고 복잡한 상황엔 한계가 있음

2. 락(Lock) 기반 동기화

  • Read/Write Lock: 여러 스레드가 동시에 읽기는 가능하지만, 쓰기는 오직 하나만 가능하게 하는 락. (예: Java의 ReentrantReadWriteLock)
  • Atomic 변수: 원자적 연산을 보장하는 변수 타입을 사용하여 경쟁 상태 예방(예: Java의 AtomicInteger).
  • Condition Variable: 특정 조건이 만족될 때까지 스레드 실행을 대기시키는 동기화 도구.

3. 기타 전략

  • 블로킹/웨이크업: 세마포어를 사용해 자원이 없으면 스레드를 블록하고, 자원이 생기면 깨움.
  • 자원 획득 순서 지정: 데드락 예방을 위해 자원 획득 순서를 명확히 지정.
  • 불변 객체(Immutable Object) 활용: 공유 자원을 변경하지 않고 읽기만 하도록 설계.
  • 스레드 로컬(Thread Local) 변수: 각 스레드가 독립적으로 데이터를 보유하도록 설계.

주의사항

  • Busy Waiting(Spin Lock): 락이 풀릴 때까지 계속 반복문을 도는 방식은 CPU 자원을 낭비할 수 있으므로, 실제로는 블로킹 방식이 더 효율적.
  • 데드락(Deadlock)과 기아(Starvation): 상호 배제 구현 시 교착상태와 특정 스레드가 영원히 자원을 얻지 못하는 현상에 주의해야 한다.
profile
백엔드 개발자를 꿈꿉니다.

0개의 댓글