경쟁 상태(Race Condition)란?
- 경쟁 상태(Race Condition)는 멀티스레드 환경에서 두 개 이상의 스레드가 동시에 공유 자원(메모리, 변수 등)에 접근하거나 수정할 때, 실행 순서에 따라 프로그램의 결과가 달라지는 현상을 의미한다.
- 예를 들어, 여러 스레드가 동시에 하나의 카운터 변수를 증가시키거나 감소시키는 경우, 예상하지 못한 결과(값의 손실, 데이터 오염 등)가 발생할 수 있다.
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): 상호 배제 구현 시 교착상태와 특정 스레드가 영원히 자원을 얻지 못하는 현상에 주의해야 한다.