반응형

멀티스레드 문제 예시 - 은행 계좌 잔액 갱신 문제

class BankAccount {
    private int balance = 100;

    public void withdraw(int amount) {
        if (balance >= amount) { // 조건 검사
            balance -= amount;  // 잔액 갱신
        }
    }
}

의도된 동작

  1. 스레드 A가 50을 출금.
  2. 스레드 B가 50을 출금.
  3. 결과적으로 잔액은 0이 되어야 함.

문제 상황

  • 스레드 A와 스레드 B가 동시에 withdraw(50) 메서드를 호출하면, 잔액 조건 검사와 갱신 작업이 중첩될 수 있음.
  • 이로 인해 두 스레드가 잔액 조건을 동시에 검사한 후, 동시에 출금을 진행.
  • 결과적으로, 잔액이 50으로 잘못 계산됨.

 

**정상적인 단일 스레드 실행**

-------
스레드 A 실행:
[검사] 잔액: 100 >= 50  -> True
[갱신] 잔액 -= 50       -> 잔액: 50

**멀티스레드에서의 충돌**
스레드 A 실행:                          스레드 B 실행:
[검사] 잔액: 100 >= 50  -> True        [검사] 잔액: 100 >= 50  -> True
[갱신] 잔액 -= 50       -> 잔액: 50    [갱신] 잔액 -= 50       -> 잔액: 50

결과: 두 스레드가 모두 출금을 완료했지만, 최종 잔액은 50으로 계산됨 (잘못된 결과).

멀티스레드 환경의 위험 요소

  1. Race Condition (경쟁 상태)
    • 여러 스레드가 동시에 동일한 자원에 접근하고 이를 수정할 때, 실행 순서에 따라 결과가 달라질 수 있음.
    • 예: 위의 은행 계좌 예제에서 스레드 간 동기화가 없을 경우 발생.
  2. Deadlock (교착 상태)
    • 두 스레드가 서로 자원을 기다리며 무한히 대기하는 상황.
    • 예: 스레드 A가 자원 1을 점유하고 자원 2를 기다리는 동안, 스레드 B는 자원 2를 점유하고 자원 1을 기다림.
  3. Data Corruption (데이터 손상)
    • 여러 스레드가 동일한 메모리 위치를 수정하면, 결과값이 예기치 않게 손상될 수 있음.
    • 예: 공유 데이터 구조의 상태가 불완전하거나 잘못된 상태로 유지됨.
  4. Thread Interleaving (스레드 중첩 실행)
    • 스레드가 실행 중에 문맥 교환(Context Switching)으로 인해 실행 흐름이 중첩됨.
    • 예: 스레드 A와 B가 교차 실행되며 연산이 중간 상태에서 중단됨.

해결 방법

1. 동기화 사용

  • synchronized 키워드: 공유 자원에 한 번에 하나의 스레드만 접근하도록 보장.
  • ReentrantLock: 더 세밀한 락 제어 가능.
public synchronized void withdraw(int amount) {
    if (balance >= amount) {
        balance -= amount;
    }
}

2. 원자적 연산 사용

  • Java의 AtomicInteger 또는 AtomicLong과 같은 클래스 사용.
  • 내부적으로 CAS(Compare-And-Swap)를 활용하여 동기화 문제를 해결.
import java.util.concurrent.atomic.AtomicInteger;

class BankAccount {
    private AtomicInteger balance = new AtomicInteger(100);

    public void withdraw(int amount) {
        balance.addAndGet(-amount); // 원자적 연산
    }
}

3. 동시성 제어 도구

  • CountDownLatch, Semaphore, CyclicBarrier 등 사용.
  • 특정 조건에서 스레드의 실행을 제어하여 동기화 문제를 방지.
반응형

+ Recent posts