synchronized 대기
- 대기 1: 락 획득 대기
- "BLOCKED" 상태로 락 획득 대기
- synchronized를 시작할 때 락이 없으면 대기
- 다른 스레드가 synchronized를 빠져나갈 때 대기가 풀리며 락 획득 시도
- 대기 2: wait() 대기
- "WAITING" 상대로 대기
- wait()를 호출했을 때 스레드 대기 집합에서 대기
- 다른 스레드가 notify()를 호출했을 때 빠져나감

- 소비자 스레드: c1 , c2 , c3
- 생산자 스레드: p1 , p2 , p3

- 소비자 스레드 c1 , c2 , c3 가 동시에 실행된다고 가정하자.
- 소비자 스레드 c1 이 가장 먼저 락을 획득한다.
- c2 , c3는 락 획득을 대기하며 "BLOCKED" 상태가 된다.
c2 , c3는 락 획득을 시도하지만, 모니터 락이 없기 때문에 락을 대기하며 "BLOCKED" 상태가 된다.
c1 은 나중에 락을 반납할 것이다. 그러면 c2 , c3 중에 하나가 락을 획득해야 한다. 그런데 잘 생각해 보면 락을 기다리는 c2 , c3 도 어딘가에서 관리가 되어야 한다. 그래야 락이 반환되었을 때 자바가 c2 , c3 중에 하나를 선택해서 락을 제공할 수 있다. 예를 들어서 List , Set , Queue 같은 자료구조에 관리가 되어야 한다. 그림에서는 c2 , c3 가 단순히 "BLOCKED" 상태로 변경만 되었다. 그래서 관리되는 것처럼 보이지는 않는다.
락 대기 집합이 포함된 synchronized 대기

- 이 그림은 이전 그림과 같은 상태를 좀 더 자세히 그린 그림이다.
- 그림을 보면 락 대기 집합이라는 곳이 있다. 이곳은 락을 기다리는 "BLOCKED" 상태의 스레드들을 관리한다.
- 락 대기 집합은 자바 내부에 구현되어 있기 때문에 모니터 락과 같이 개발자가 확인하기는 어렵다.
- 여기서는 "BLOCKED" 상태의 스레드 c2 , c3 가 관리된다.
- 언젠가 c1 이 락을 반납하면 락 대기 집합에서 관리되는 스레드 중 하나가 락을 획득한다.
락 대기 집합을 지금 설명하는 이유
참고로 지금까지는 스레드를 최대한 쉽고 단순하게 설명하기 위해 "BLOCKED" 상태에서 사용하는 락 대기 집합을 일부러 설명하지 않았다. 이제 여러분이 스레드를 어느 정도 이해했기 때문에 락 대기 집합의 개념을 설명해도 이해하는데 어려움은 없을 것이다.
사실 락 대기 집합에 대한 내용을 몰라도 괜찮다. 다만 지금 이 내용을 풀어서 설명하는 이유는 스레드가 모니터 락을 기다리는 상태와 Object.wait()를 통한 대기 상태를 헷갈릴 수 있기 때문이다. 이 부분을 명확히 하기 위해 풀어서 설명한다.


- c1은 큐에 획득할 데이터가 없기 때문에 락을 반납하고, "WAITING "상태로 스레드 대기 집합에서 대기한다.

- 이후에 락 대기 집합에 있는 c2가 락을 획득하고, 임계 영역을 수행한다. 큐에 획득할 데이터가 없기 때문에 락을 반납하고, "WAITING" 상태로 스레드 대기 집합에서 대기한다.
- c3 도 동일한 로직을 수행한다.

- p1 이 락을 획득하고 데이터를 저장한 다음 스레드 대기 집합에 이 사실을 알린다.

- 스레드 대기 집합에 있는 c1 이 스레드 대기 집합을 빠져나간다.
- 하지만 아직 끝난 것이 아니다. 락을 얻어서 락 대기 집합까지 빠져나가야 임계 영역을 수행할 수 있다.
- c1은 락 획득을 시도하지만 락이 없다. 따라서 락 대기 집합에서 관리된다.
개념상 락 대기 집합이 1차 대기소이고, 스레드 대기 집합이 2차 대기소이다. 2차 대기소에 있는 스레드는 2차 대기소를 빠져나온다고 끝이 아니다. 1차 대기소까지 빠져나와야 임계 영역에서 로직을 수행할 수 있다. 비유를 하자면 임계 영역을 안전하게 지키기 위한 2중 감옥인 것이다. 스레드는 2중 감옥을 모두 탈출해야 임계 영역을 수행할 수 있다.

- c1 은 락 획득을 기다리며 "BLOCKED" 상태로 락 대기 집합에서 기다린다.
- 드디어 p1 이 락을 반납한다.

- 락이 반납되면 락 대기 집합에 있는 스레드 중 하나가 락을 획득한다. 여기서는 c1이 락을 획득한다.
- c1은 드디어 1차 대기소까지 탈출하고, 임계 영역을 수행한다.
자바의 모든 객체 인스턴스는 멀티스레드와 임계 영역을 다루기 위해 내부에 3가지 기본 요소를 가진다.
- 모니터 락
- 락 대기 집합(모니터 락 대기 집합)
- 스레드 대기 집합
여기서 락 대기 집합이 1차 대기소이고, 스레드 대기 집합이 2차 대기소라 생각하면 된다. 2차 대기소에 들어간 스레드는 2차, 1차 대기소를 모두 빠져나와야 임계 영역을 수행할 수 있다.
이 3가지 요소는 서로 맞물려 돌아간다.
- synchronized를 사용한 임계 영역에 들어가려면 모니터 락이 필요하다.
- 모니터 락이 없으면 락 대기 집합에 들어가서 "BLOCKED" 상태로 락을 기다린다.
- 모니터 락을 반납하면 락 대기 집합에 있는 스레드 중 하나가 락을 획득하고 "BLOCKED" -> "RUNNABLE" 상태가 된다.
- wait()를 호출해서 스레드 대기 집합에 들어가기 위해서는 모니터 락이 필요하다.
- 스레드 대기 집합에 들어가면 모니터 락을 반납한다.
- 스레드가 notify()를 호출하면 스레드 대기 집합에 있는 스레드 중 하나가 스레드 대기 집합을 빠져나온다. 그리고 모니터 락 획득을 시도한다.
- 모니터 락을 획득하면 임계 영역을 수행한다.
- 모니터 락을 획득하지 못하면 락 대기 집합에 들어가서 "BLOCKED" 상태로 락을 기다린다.
synchronized vs ReentrantLock 대기
synchronized 대기

- 대기 1: 모니터 락 획득 대기
- 자바 객체 내부의 락 대기 집합(모니터 락 대기 집합)에서 관리
- "BLOCKED" 상태로 락 획득 대기
- synchronized를 시작할 때 락이 없으면 대기
- 다른 스레드가 synchronized를 빠져나갈 때 락을 획득 시도, 락을 획득하면 락 대기 집합을 빠져나감
- 대기 2: wait() 대기
- wait()를 호출했을 때 자바 객체 내부의 스레드 대기 집합에서 관리
- "WAITING" 상태로 대기
- 다른 스레드가 notify()를 호출했을 때 스레드 대기 집합을 빠져나감
ReentrantLock 대기

- 대기 1: ReentrantLock 락 획득 대기
- ReentrantLock의 대기 큐에서 관리
- "WAITING" 상태로 락 획득 대기
- lock.lock()을 호출했을 때 락이 없으면 대기
- 다른 스레드가 lock.unlock()을 호출했을 때 대기가 풀리며 락 획득 시도, 락을 획득하면 대기 큐를 빠져나감
- 대기 2: await() 대기
- condition.await()를 호출했을 때, condition 객체의 스레드 대기 공간에서 관리
- "WAITING" 상대로 대기
- 다른 스레드가 condition.signal()을 호출했을 때 condition 객체의 스레드 대기 공간에서 빠져나감
2단계 대기소
참고로 깨어난 스레드는 바로 실행되는 것이 아니다. synchronized와 마찬가지로 ReentrantLock 도 대기소가 2단계로 되어 있다. 2단계 대기소인 condition 객체의 스레드 대기 공간을 빠져나온다고 바로 실행되는 것이 아니다.
임계 영역 안에서는 항상 락이 있는 하나의 스레드만 실행될 수 있다. 여기서는 ReentrantLock의 락을 획득해야 "RUNNABLE" 상태가 되면서 그다음 코드를 실행할 수 있다. 락을 획득하지 못하면 "WAITING" 상태로 락을 획득할 때까지 ReentrantLock의 대기 큐에서 대기한다.
'멀티스레드와 동시성' 카테고리의 다른 글
| Ch10. 동기화와 원자적 연산(CAS) - 원자적 연산 (0) | 2024.09.14 |
|---|---|
| Ch09. 생산자 소비자 문제 - BlockingQueue (0) | 2024.08.27 |
| Ch09. 생산자 소비자 문제 - 생산자/소비자 대기 공간 분리 (0) | 2024.08.24 |
| Ch09. 생산자 소비자 문제 - Lock Condition (0) | 2024.08.24 |
| Ch08. 생산자 소비자 문제 - wait, notify의 한계 (0) | 2024.08.23 |