이런 문제가 발생한 근본 원인은 여러 스레드가 함께 사용하는 공유 자원을 여러 단계로 나누어 사용하기 때문이다.
- 검증 단계: 잔액( balance )이 출금액( amount ) 보다 많은지 확인한다.
- 출금 단계: 잔액( balance )을 출금액( amount ) 만큼 줄인다.
출금() {
//검증 단계: 잔액(balance) 확인
//출금 단계: 잔액(balance) 감소
}
이 로직에는 하나의 큰 가정이 있다.
스레드 하나의관점에서 출금()을보면검증 단계에서 확인한 잔액( balance ) 1000원은 출금 단계에서 계산을 끝마칠 때까지 같은 1000원으로 유지되어야 한다. 그래야 검증 단계에서 확인한 금액으로, 출금 단계에서 정확한 잔액을 계산할 수 있다.
그래야 검증 단계에서 확인한 1000원에 800원을 차감해서 200원이라는 잔액을 정확하게 계산할 수 있다. 결국 여기서는 내가 사용하는 값이 중간에 변경되지 않을 것이라는 가정이 있다.
그런데 만약 중간에 다른 스레드가 잔액의 값을 변경한다면, 큰 혼란이 발생한다. 1000원이라 생각한 잔액이 다른 값으로 변경되면 잔액이 전혀 다른 값으로 계산될 수 있다.
공유 자원
잔액( balance )은 여러 스레드가 함께 사용하는 공유 자원이다. 따라서 출금 로직을 수행하는 중간에 다른 스레드에서 이 값을 얼마든지 변경할 수 있다. 참고로 여기서는 출금() 메서드를 호출할 때만 잔액( balance )의 값이 변경된다. 따라서 다른 스레드가 출금 메서드를 호출하면서, 사용 중인 출금 값을 중간에 변경해 버릴 수 있다.
한 번에 하나의 스레드만 실행
만약 출금()이라는 메서드를 한 번에 하나의 스레드만 실행할 수 있게 제한한다면 어떻게 될까?
예를 들어 t1 , t2 스레드가 함께 출금()을 호출하면 t1 스레드가 먼저 처음부터 끝까지 출금() 메서드를 완료하고, 그다음에 t2 스레드가 처음부터 끝까지 출금() 메서드를 완료하는 것이다.
이렇게 하면 공유 자원인 balance를 한 번에 하나의 스레드만 변경할 수 있다. 따라서 계산 중간에 다른 스레드가 balance의 값을 변경하는 부분을 걱정하지 않아도 된다. (참고로 여기서는 출금() 메서드를 호출할 때만 잔액(balance)의 값이 변경된다.)
- 더 자세히는 출금을 진행할 때 잔액( balance )을 검증하는 단계부터 잔액의 계산을 완료할 때까지 잔액의 값은 중간에 변하면 안 된다.
- 이 검증과 계산 이 두 단계는 한 번에 하나의 스레드만 실행해야 한다. 그래야 잔액(balance )이 중간에 변하지 않고, 안전하게 계산을 수행할 수 있다.
임계 영역(critical section)
- 여러 스레드가 동시에 접근하면 데이터 불일치나 예상치 못한 동작이 발생할 수 있는 위험하고 또 중요한 코드 부분을 뜻한다.
- 여러 스레드가 동시에 접근해서는 안 되는 공유 자원을 접근하거나 수정하는 부분을 의미한다.
- 예) 공유 변수나 공유 객체를 수정
앞서 우리가 살펴본 출금() 로직이 바로 임계 영역이다. 더 자세히는 출금을 진행할 때 잔액( balance )을 검증하는 단계부터 잔액의 계산을 완료할 때 까지가 임계 영역이다. 여기서 balance는 여러 스레드가 동시에 접근해서는 안 되는 공유 자원이다. 이런 임계 영역은 한 번에 하나의 스레드만 접근할 수 있도록 안전하게 보호해야 한다.
그럼 어떻게 한 번에 하나의 스레드만 접근할 수 있도록 임계 영역을 안전하게 보호할 수 있을까? 여러가지 방법이 있지만 자바는 synchronized 키워드를 통해 아주 간단하게 임계 영역을 보호할 수 있다
'멀티스레드와 동시성' 카테고리의 다른 글
| Ch06. 동기화 - 출금 (0) | 2024.08.11 |
|---|---|
| Ch06. 동기화 - synchronized (0) | 2024.08.11 |
| Ch06. 동기화 - 동시성 문제 (0) | 2024.08.10 |
| Ch05. 메모리 가시성 - 자바 메모리 모델 (0) | 2024.08.08 |
| Ch05. 메모리 가시성 - volatile 예시 (0) | 2024.08.08 |