이번에는 각 상황에 맞추어 스레드가 기다리도록 해보자.
package thread.bounded;
import java.util.ArrayDeque;
import java.util.Queue;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
public class BoundedQueueV2 implements BoundedQueue {
private final Queue<String> queue = new ArrayDeque<>();
private final int max;
public BoundedQueueV2(int max) {
this.max = max;
}
@Override
public synchronized void put(String data) {
while (queue.size() == max) {
log("[put] 큐가 가득 참, 생산자 대기");
sleep(1000);
}
queue.offer(data);
}
@Override
public synchronized String take() {
while (queue.isEmpty()) {
log("[take] 큐에 데이터가 없음, 소비자 대기");
sleep(1000);
}
return queue.poll();
}
@Override
public String toString() {
return queue.toString();
}
}
put(data) - 데이터를 버리지 않는 대안
data3을 버리지 않는 대안은, 큐가 가득 찾을 때, 큐에 빈 공간이 생길 때까지 , 생산자 스레드를 기다리면 된다. 언젠가는 소비자 스레드가 실행되어서 큐의 데이터를 가져갈 것이고, 그러면 큐에 데이터를 넣을 수 있는 공간이 생기게 된다.
그럼 어떻게 기다릴 수 있을까?
여기서 생산자 스레드가 반복문을 사용해서 큐에 빈 공간이 생기는지 주기적으로 체크한다. 만약 빈 공간이 없다면 sleep()을 사용해서 잠시 대기하고, 깨어난 다음에 다시 반복문에서 큐의 빈 공간을 체크하는 식으로 구현했다.
take() - 큐에 데이터가 없다면 기다리자
소비자 입장에서 큐에 데이터가 없다면 기다리는 것도 대안이다.
큐에 데이터가 없을 때 null 을 받지 않는 대안은 큐에 데이터가 추가될 때까지 소비자 스레드가 기다리는 것이다. 언젠가는 생산자 스레드가 실행되어서 큐의 데이터를 추가할 것이고, 큐에 데이터가 생기게 된다. 물론 생산자 스레드가 계속해서 데이터를 생산한다는 가정이 필요하다.
그럼 어떻게 기다릴 수 있을까?
여기서는 소비자 스레드가 반복문을 사용해서 큐에 데이터가 있는지 주기적으로 체크한 다음에, 만약 데이터가 없다면 sleep()을 사용해서 잠시 대기하고, 깨어난 다음에 다시 반복문에서 큐에 데이터가 있는지 체크하는 식으로 구현했다.
BoundedMain - BoundedQueueV2를 사용하도록 변경
public class BoundedMain {
public static void main(String[] args) {
// 1. BoundedQueue 선택
//BoundedQueue queue = new BoundedQueueV1(2);
BoundedQueue queue = new BoundedQueueV2(2);
// 2. 생산자, 소비자 실행 순서 선택, 반드시 하나만 선택!
producerFirst(queue); // 생산자 먼저 실행
//consumerFirst(queue); // 소비자 먼저 실행
}
- BoundedQueueV2 를 사용하도록 변경하자
- 생산자 먼저 실행하도록 주석을 변경하자


실행 결과를 보면 생산자가 계속 반복된다.
문제 - 생산자 먼저 실행의 경우
producer3 이 종료되지 않고 계속 수행되고, consumer1, consumer2 , consumer3 은 BLOCKED 상태가 된다.
실행 결과 - BoundedQueueV2, 소비자 먼저 실행

문제 - 소비자 먼저 실행의 경우
소비자 먼저 실행의 경우 consumer1 이 종료되지 않고 계속 수행된다. 그리고 나머지 모든 스레드가 BLOCKD 상태가 된다.
생산자 소비자 문제 - 예제2 분석
BoundedQueueV2 - 생산자 먼저 실행 분석
실행 결과 - BoundedQueueV2, 생산자 먼저 실행
18:02:26.315 [ main] == [생산자 먼저 실행] 시작, BoundedQueueV2 ==
18:02:26.344 [ main] 생산자 시작
18:02:26.380 [producer1] [생산 시도] data1 -> []
18:02:26.381 [producer1] [생산 완료] data1 -> [data1]
18:02:26.471 [producer2] [생산 시도] data2 -> [data1]
18:02:26.472 [producer2] [생산 완료] data2 -> [data1, data2]
18:02:26.585 [producer3] [생산 시도] data3 -> [data1, data2]
18:02:26.586 [producer3] [put] 큐가 가득 참, 생산자 대기
18:02:26.696 [ main] 현재 상태 출력, 큐 데이터: [data1, data2]
18:02:26.711 [ main] producer1: TERMINATED
18:02:26.712 [ main] producer2: TERMINATED
18:02:26.712 [ main] producer3: TIMED_WAITING
18:02:26.713 [ main] 소비자 시작
18:02:26.716 [consumer1] [소비 시도] ? <- [data1, data2]
18:02:26.817 [consumer2] [소비 시도] ? <- [data1, data2]
18:02:26.924 [consumer3] [소비 시도] ? <- [data1, data2]
18:02:27.033 [ main] 현재 상태 출력, 큐 데이터: [data1, data2]
18:02:27.036 [ main] producer1: TERMINATED
18:02:27.039 [ main] producer2: TERMINATED
18:02:27.040 [ main] producer3: TIMED_WAITING
18:02:27.041 [ main] consumer1: BLOCKED
18:02:27.041 [ main] consumer2: BLOCKED
18:02:27.043 [ main] consumer3: BLOCKED
18:02:27.044 [ main] == [생산자 먼저 실행] 종료, BoundedQueueV2 ==
18:02:27.597 [producer3] [put] 큐가 가득 참, 생산자 대기
18:02:28.604 [producer3] [put] 큐가 가득 참, 생산자 대기
18:02:29.608 [producer3] [put] 큐가 가득 참, 생산자 대기
18:02:30.611 [producer3] [put] 큐가 가득 참, 생산자 대기

생산자 스레드 실행 시작

18:02:26.315 [ main] == [생산자 먼저 실행] 시작, BoundedQueueV2 ==
18:02:26.344 [ main] 생산자 시작
18:02:26.380 [producer1] [생산 시도] data1 -> []

18:02:26.381 [producer1] [생산 완료] data1 -> [data1]

18:02:26.471 [producer2] [생산 시도] data2 -> [data1]

18:02:26.472 [producer2] [생산 완료] data2 -> [data1, data2]

18:02:26.585 [producer3] [생산 시도] data3 -> [data1, data2]
18:02:26.586 [producer3] [put] 큐가 가득 참, 생산자 대기
- 생산자 스레드인 p3 인 임계영역에 들어가기 위해 먼저 락을 획득한다.
- 큐에 data3을 저장하려고 시도한다. 그런데 큐가 가득 차 있다.
- p3는 sleep(1000)을 사용해서 잠시 대기한다. 이때 RUNNABLE -> TIME_WAITING 상태가 된다.
- 이때 반복문을 사용해서 sleep()으로 잠시 대기한 다음 반복문을 계속해서 수행한다. 1초마다 한 번씩 체크하기 때문에 "큐가 가득 참, 생산자 대기"라는 메시지가 계속 출력될 것이다.
여기서 핵심은 p3 스레드가 락을 가지고 있는 상태에서, 큐에 빈자리가 나올 때까지 대기한다는 점이다.
18:02:26.696 [ main] 현재 상태 출력, 큐 데이터: [data1, data2]
18:02:26.711 [ main] producer1: TERMINATED
18:02:26.712 [ main] producer2: TERMINATED
18:02:26.712 [ main] producer3: TIMED_WAITING

18:02:26.713 [ main] 소비자 시작
18:02:26.716 [consumer1] [소비 시도] ? <- [data1, data2]

무한 대기 문제
- c1 이 임계 영역에 들어가기 위해 락을 획득하려 한다.
- 그런데 락이 없다. 왜냐하면 p3 가 락을 가지고 임계영역에 이미 들어가 있기 때문이다. p3 가 락을 반납하기 전까지는 c1 은 절대로 임계영역 (여기서는 synchronized)에 들어갈 수 없다.
- 여기서 심각한 무한 대기 문제가 발행한다.
- p3 가 락을 반납하려면 -> 소비자 스레드인 c1 이 먼저 작동해서 큐의 데이터를 가져가야 한다.
- 소비자 스레드인 c1 이 락을 획득하려면 -> 생산자 스레드인 p3 가 먼저 락을 반납해야 한다.
- p3는 락을 반납하지 않고, c1 은 큐의 데이터를 가져갈 수 없다.
- 지금 상태면 p3 은 절대로 락을 반납할 수 없다. 왜냐하면 락을 반납하려면 c1 이 먼저 큐의 데이터를 소비해야 한다.
- 그래야 p3 큐에 data3을 저장하고 임계영역을 빠져나가며 락을 반납할 수 있다. 그런데 p3 가 락을 가지고 임계영역 안에 있기 때문에, 임계 영역 밖의 c1 은 락을 획득할 수 없으므로, 큐에 접근하지 못하고 무한 대기한다.
- 결과적으로 소비자 스레드인 c1 은 p3 가 락을 반납할 때까지 BLOCKED 상태로 대기한다

18:02:26.817 [consumer2] [소비 시도] ? <- [data1, data2]
c2 도 마찬가지로 락을 얻을 수 없으므로 BLOCKED 상태로 대기한다.

18:02:26.924 [consumer3] [소비 시도] ? <- [data1, data2]
c3 도 마찬가지로 락을 얻을 수 없으므로 BLOCKED 상태로 대기한다
소비자 스레드 실행 완료
18:02:27.033 [ main] 현재 상태 출력, 큐 데이터: [data1, data2]
18:02:27.036 [ main] producer1: TERMINATED
18:02:27.039 [ main] producer2: TERMINATED
18:02:27.040 [ main] producer3: TIMED_WAITING
18:02:27.041 [ main] consumer1: BLOCKED
18:02:27.041 [ main] consumer2: BLOCKED
18:02:27.043 [ main] consumer3: BLOCKED
18:02:27.044 [ main] == [생산자 먼저 실행] 종료, BoundedQueueV2 ==
18:02:27.597 [producer3] [put] 큐가 가득 참, 생산자 대기
18:02:28.604 [producer3] [put] 큐가 가득 참, 생산자 대기
18:02:29.608 [producer3] [put] 큐가 가득 참, 생산자 대기
18:02:30.611 [producer3] [put] 큐가 가득 참, 생산자 대기
18:02:31.625 [producer3] [put] 큐가 가득 참, 생산자 대기
결과적으로 c1 , c2 , c3는 모두 락을 획득하기 위해 BLOCKED 상태로 대기한다.
p3는 1초마다 한번씩 깨어나서 큐의 상태를 확인한다. 그런데 본인이 락을 가지고 있기 때문에 다른 스레드가 임계영역 안에 들어오는 것은 불가능하다. 따라서 다른 스레드는 임계영역 안에 있는 큐에 접근조차 할 수 없다.
결국 p3 는 절대로 비워지지 않는 큐를 계속 확인하게 된다. 그리고 [put] 큐가 가득 참, 생산자 대기를 1초마다 출력한다.
결국 이런 상태가 무한하게 지속된다
BoundedQueueV2 - 소비자 먼저 실행 분석
실행 결과 - BoundedQueueV2, 소비자 먼저 실행
18:34:44.525 [ main] == [소비자 먼저 실행] 시작, BoundedQueueV2 ==
18:34:44.554 [ main] 소비자 시작
18:34:44.571 [consumer1] [소비 시도] ? <- []
18:34:44.572 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
18:34:44.669 [consumer2] [소비 시도] ? <- []
18:34:44.779 [consumer3] [소비 시도] ? <- []
18:34:44.891 [ main] 현재 상태 출력, 큐 데이터: []
18:34:44.910 [ main] consumer1: TIMED_WAITING
18:34:44.911 [ main] consumer2: BLOCKED
18:34:44.912 [ main] consumer3: BLOCKED
18:34:44.912 [ main] 생산자 시작
18:34:44.932 [producer1] [생산 시도] data1 -> []
18:34:45.016 [producer2] [생산 시도] data2 -> []
18:34:45.124 [producer3] [생산 시도] data3 -> []
18:34:45.239 [ main] 현재 상태 출력, 큐 데이터: []
18:34:45.242 [ main] consumer1: TIMED_WAITING
18:34:45.243 [ main] consumer2: BLOCKED
18:34:45.244 [ main] consumer3: BLOCKED
18:34:45.245 [ main] producer1: BLOCKED
18:34:45.245 [ main] producer2: BLOCKED
18:34:45.245 [ main] producer3: BLOCKED
18:34:45.247 [ main] == [소비자 먼저 실행] 종료, BoundedQueueV2 ==
18:34:45.584 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
18:34:46.588 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
18:34:47.589 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
18:34:48.601 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기

소비자 스레드 실행 시작

18:34:44.554 [ main] 소비자 시작
18:34:44.571 [consumer1] [소비 시도] ? <- []
18:34:44.572 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
- 소비자 스레드인 c1 은 임계영역에 들어가기 위해 락을 획득한다.
- c1은 큐의 데이터를 획득하려 하지만, 데이터가 없다.
- c1 은 sleep(1000)을 사용해서 잠시 대기한다. 이때 RUNNABLE -> TIME_WAITIN 상태가 된다.
- 이때 반복문을 사용해서 1초마다 큐에 데이터가 있는지 반복해서 확인한다,
- 데이터가 있다면 큐의 데이터를 가져오고 완료된다.
- 데이터가 없다면 반복문을 계속해서 수행한다. 1초마다 한번 "큐에 데이터가 없음, 소비자 대기"라는 메시지가 출력될 것이다.

18:34:44.669 [consumer2] [소비 시도] ? <- []
18:34:44.779 [consumer3] [소비 시도] ? <- []
18:34:44.891 [ main] 현재 상태 출력, 큐 데이터: []
18:34:44.910 [ main] consumer1: TIMED_WAITING
18:34:44.911 [ main] consumer2: BLOCKED
18:34:44.912 [ main] consumer3: BLOCKED
무한 대기 문제
c2 , c3 가 임계 영역에 들어가기 위해 락을 획득하려 한다. c1 이 락을 반납하기 전까지는 c2, c3는 절대로 임계영역 (여기서는 synchronized) 은 들어갈 수 없다!
여기서 심각한 무한 대기 문제가 발생한다.
c1 이 락을 반납하지 않기 때문에 c2, c3 은 BLOCKED 상태가 된다.

18:34:44.912 [ main] 생산자 시작
18:34:44.932 [producer1] [생산 시도] data1 -> []
18:34:45.016 [producer2] [생산 시도] data2 -> []
18:34:45.124 [producer3] [생산 시도] data3 -> []
18:34:45.239 [ main] 현재 상태 출력, 큐 데이터: []
18:34:45.242 [ main] consumer1: TIMED_WAITING
18:34:45.243 [ main] consumer2: BLOCKED
18:34:45.244 [ main] consumer3: BLOCKED
18:34:45.245 [ main] producer1: BLOCKED
18:34:45.245 [ main] producer2: BLOCKED
18:34:45.245 [ main] producer3: BLOCKED
18:34:45.247 [ main] == [소비자 먼저 실행] 종료, BoundedQueueV2 ==
18:34:45.584 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
18:34:46.588 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
18:34:47.589 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
18:34:48.601 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
무한 대기 문제
- p1 , p2 , p3 가 임계 영역에 들어가기 위해 락을 획득하려 한다.
- 그런데 락이 없다. 왜냐하면 c1d이 락을 가지고 임계영역에 들어가 있기 때문이다. c1 이 락을 반납하기 전까지는 p1, p2, p3는 절대로 임계영역에 들어갈 수 없다.
- 여기서 심각한 대기문제가 발생한다.
- c1 은 락을 반납하지 않고, p1 은 큐에 데이터를 추가할 수 없다. (물론 p2 , p3 도 포함이다.)
- 지금 상태면 c1 은 절대로 락을 반납할 수 없다. 왜냐하면 락을 반납하려면 p1 이 먼저 큐의 데이터를 추가해야 한다.
- 그래야 c1 이 큐에 데이터를 획득하고 임계 영역을 빠져나가며 락을 반납할 수 있다. 그런데 c1 이 락을 가지고 임계 영역 안에 있기 때문에 , 임계 영역 밖의 p1 은 락을 획득할 수 없으므로, 큐에 접근하지 못하고 무한대기한다.
- 결과적으로 생산자 스레드 p1 은 c1 이 락은 반납할 때까지 BLOCK 상태로 대기한다.
결과적으로 c1을 제외한 모든 스레드가 락을 획득하기 위해 BLOCKED 상태로 대기한다. c1 은 1초마다 한 번씩 깨어나서 큐의 상태를 확인한다. 그런데 본인이 락을 가지고 있기 때문에 다른 스레드는 임계 영 역에 들어오는 것이 불가능하고, 큐에 접근조차 할 수 없다. 따라서 [take] 큐에 데이터가 없음, 소비자 대기를 1초마다 계속 출력한다. 결국 이런 상태가 무한하게 지속된다.
정리
버퍼가 비어를 때 소비하거나, 버퍼가 가득 찾을 때 생산하는 문제를 해결하기 위해, 단순히 스레드가 잠깐 기다리면 되러 이라 생각했는데 , 문제가 더 심각해졌다. 생각해 보면 결국 임계영역 안에서 락을 가지고 대기하는 것이 문제이다. 이것은 마치 열쇠를 가지 사람이 안에서 문을 잠가버린 것과 같다. 그래서 다른 스레드가 임계 영역 안에 접근조차 할 수 없다는 것이다.
여기서 잘 생각해 보면, 락을 가지고 임계영역 안에서 스레드가 sleep()을 호출해서 잠시 대기할 때는 아무 일도 하지 않는다. 그렇다면 아무일도 하지 않고 대기하는 잠시 다른 스레드에게 락을 양보하며 어떨까? 그러면 다른 스레드가 버퍼에 값을 채우거나 버퍼의 값을 가져갈 수 있다.
예를 들어 락을 가진 소비자 스레드가 임계 영역 안에서 버퍼의 값을 획득하기를 기다린다고 가정하자. 버퍼에 값이 없으면 값이 채워질 때까지 소비자 스레드는 아무 일도 하지 않고 대기해야 한다. 어차피 아무일도 하지 않으므로, 이때 잠시 락을 다른 스레드에게 빌려주는 것이다. 락을 획득한 생산자 스레드는 이때 버퍼에 값을 채우고 락을 반납한다. 버퍼에 값이 차면 대기하던 소비자 스레드가 다시 락을 획득한 다음에 버퍼의 값을 가져가고 락을 반납한다. 버퍼에 값이 차면 대기하던 소비자 스레드가 다시 락을 획득한 다음에 버퍼의 값을 가져가고 락을 반납하는 것이다.
Object - wait, notify - 예제 3 코드
자바는 처음부터 멀티스레드를 고려하며 탄생한 언어다.
앞서 설명한 synchronized를 사용한 임계 영역 안에서 락을 가지고 무한 대기하는 문제는 흥미롭게도 Object 클 래스에 해결 방안이 있다. Object 클래스는 이런 문제를 해결할 수 있는 wait() , notify()라는 메서드를 제공한다. Object는 모든 자바 객체의 부모이기 때문에, 여기 있는 기능들은 모두 자바 언어의 기본 기능이라 생각하면 된다.
wait(), notify() 설명
- Object.wait()
- 현재 스레드가 가진 락을 반납하고 대기 (WAITING) 한다.
- 현재 스레드를 대기 (WAITING) 상태로 전환한다. 이 메서드는 현재 스레드가 synchronized 블록이나 메서드에서 락을 소유하고 있을 때만 호출할 수 있다. 호출한 스레드는 락을 반납하고, 다른 스레드가 해당락을 획득할 수 있도록 한다. 이렇게 대기상태로 전환된 스레드는 다른 스레드가 notify() 또는 notifyAll() 을 호출할 때 까지 대기 상태를 유지한다.
- Object.notify()
- 대기 중인 스레드 중 하나를 깨운다.
- 이 메서드는 synchronized 블록이나 메서드에서 호출되어야 한다. 깨운 스레드는 락을 다시 획득할 기회를 얻게 된다. 만약 대기 중인 스레드가 여러개라면, 그 중 하나만 깨어지게 된다.
- Object.notifyAll()
- 대기 중인 모든 스레드를 깨운다.
- 이 메서드 역시 synchronized 블록이나 메서드에서 호출되어야 하며, 모든 대기 중인 스레드가 락을 획득할 수 있는 기회를 얻게 된다. 이 방법은 모드 스레드를 깨워야 할 필요가 있는 경우 유용하다.
wait() , notify() 메서드를 적절히 사용하면, 멀티스레드 환경에서 발생할 수 있는 문제를 효율적으로 해결할 수 있다.
이 기능을 활용해서 스레드가 락을 가지고 임계 영역안에서 무한 대기하는 문제를 해결해보자.
package thread.bounded;
import java.util.ArrayDeque;
import java.util.Queue;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
public class BoundedQueueV3 implements BoundedQueue {
private final Queue<String> queue = new ArrayDeque<>();
private final int max;
public BoundedQueueV3(int max) {
this.max = max;
}
@Override
public synchronized void put(String data) {
while (queue.size() == max) {
log("[put] 큐가 가득 참, 생산자 대기");
try {
wait(); // RUNNABLE -> WAITING, 락 반납
log("[put] 생산자 깨어남");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
queue.offer(data);
log("[put] 생산자 데이터 저장, notify() 호출");
notify(); // 대기 스레드, WAIT -> BLOCKED
}
@Override
public synchronized String take() {
while (queue.isEmpty()) {
log("[take] 큐에 데이터가 없음, 소비자 대기");
try {
wait();
log("[take] 소비자 깨어남");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
String data = queue.poll();
log("[take] 소비자 데이터 획득, notify() 호출");
notify(); // 대기 스레드, WAIT -> BLOCKED
return data;
}
@Override
public String toString() {
return queue.toString();
}
}
앞서 작성한 sleep() 코드는 제거하고 대신에 Object.wait() 를 사용하자. Object 는 모든 클래스의 부모이므로 자바의 모든 객체는 해당 기능을 사용할 수 있다.
put(data) - wait(), notify()
- synchronized 를 통해 임계 영역을 설정한다. 생산자 스레드는 락 획득을 시도한다.
- 락을 획득한 생산자 스레드는 반복문을 사용해 큐에 빈 공간이 생기는지 주기적으로 체크한다. 만약 빈 공간이 없다면 Object.wait() 을 사용해서 대기한다. 참고로 대기할 때 락을 반납하고 대기한다. 그리고 대기 상태에서 깨어나면, 다시 반복문에서 큐의 빈 공간을 체크한다.
- wait() 를 호출해서 대기하는 경우 RUNNABLE -> WAITING 상태가 된다.
- 생산자가 데이터를 큐에 저장하고 notify() 를 통해 저장된 데이터가 있다고 대기하는 스레드에 알려주어야한다. 이때 notify() 를 호출하면 소비자 스레드는 깨어나서 저장된 데이터를 획득할 수 있다.
take() - wait(), notify()
synchronized 를 통해 임계 영역을 설정한다. 소비자 스레드는 락획득을 시도한다.
락을 획득한 소비자 스레드는 반복문을 사용해서 큐에 데이터가 있는지 주기적으로 체크한다. 만약 데이터가 없다면 Object.wait() 을 사용해서 대기한다. 참고로 대기할 때 락을 반납하고 대기한다. 그리고 대기 상태에서 깨어나면, 다시 반복문에서 큐에 데이터가 있는지 체크한다.
소비자가 데이터를 획득하고 나면 notify() 를 통해 큐에 저장할 여유공간이 생겼다고 대기하는 스레드에게 알려주어야 한다. 예를 들어서 큐에 데이터가 가득차서 대기하는 생산자 스레드가 있다고 가정하자. 이때 notify() 를 호출하면 생산자 스레드는 깨어나서 데이터를 큐에 저장할 수 있다.
wait() 로 대기 상태에 빠진 스레드는notify() 를 사용해야 깨울 수 있다. 생산자는 생산을 완료하면 notify() 로 대기하는 스레드를 깨워서 생산된 데이터를 가져가게 하고, 소비자는 소비를 완료하면 notify() 로 대기하는 스레드를 깨워서 데이터를 생산하라고 하면 된다. 여기서 중요한 핵심은 wait() 를 호출해서 대기 상태에 빠질 때 락을 반납하고 대기 상태에 빠진다는 것이다. 대기 상태에 빠지면 어차피 아무일도 하지 않으므로 락도 필요하지 않다.
BoundedMain - BoundedQueueV3를 사용하도록 변경
//BoundedQueue queue = new BoundedQueueV2(2);
BoundedQueue queue = new BoundedQueueV3(2)
실행 결과 - BoundedQueueV3, 생산자 먼저 실행
23:13:34.766 [ main] == [생산자 먼저 실행] 시작, BoundedQueueV3 ==
23:13:34.801 [ main] 생산자 시작
23:13:34.837 [producer1] [생산 시도] data1 -> []
23:13:34.837 [producer1] [put] 생산자 데이터 저장, notify() 호출
23:13:34.838 [producer1] [생산 완료] data1 -> [data1]
23:13:34.917 [producer2] [생산 시도] data2 -> [data1]
23:13:34.919 [producer2] [put] 생산자 데이터 저장, notify() 호출
23:13:34.919 [producer2] [생산 완료] data2 -> [data1, data2]
23:13:35.030 [producer3] [생산 시도] data3 -> [data1, data2]
23:13:35.030 [producer3] [put] 큐가 가득 참, 생산자 대기
23:13:35.144 [ main] 현재 상태 출력, 큐 데이터: [data1, data2]
23:13:35.159 [ main] producer1: TERMINATED
23:13:35.160 [ main] producer2: TERMINATED
23:13:35.160 [ main] producer3: WAITING
23:13:35.161 [ main] 소비자 시작
23:13:35.165 [consumer1] [소비 시도] ? <- [data1, data2]
23:13:35.166 [consumer1] [take] 소비자 데이터 획득, notify() 호출
23:13:35.166 [producer3] [put] 생산자 깨어남
23:13:35.166 [producer3] [put] 생산자 데이터 저장, notify() 호출
23:13:35.167 [producer3] [생산 완료] data3 -> [data2, data3]
23:13:35.167 [consumer1] [소비 완료] data1 <- [data2, data3]
23:13:35.264 [consumer2] [소비 시도] ? <- [data2, data3]
23:13:35.265 [consumer2] [take] 소비자 데이터 획득, notify() 호출
23:13:35.265 [consumer2] [소비 완료] data2 <- [data3]
23:13:35.375 [consumer3] [소비 시도] ? <- [data3]
23:13:35.377 [consumer3] [take] 소비자 데이터 획득, notify() 호출
23:13:35.377 [consumer3] [소비 완료] data3 <- []
23:13:35.486 [ main] 현재 상태 출력, 큐 데이터: []
23:13:35.487 [ main] producer1: TERMINATED
23:13:35.488 [ main] producer2: TERMINATED
23:13:35.488 [ main] producer3: TERMINATED
23:13:35.491 [ main] consumer1: TERMINATED
23:13:35.492 [ main] consumer2: TERMINATED
23:13:35.494 [ main] consumer3: TERMINATED
23:13:35.495 [ main] == [생산자 먼저 실행] 종료, BoundedQueueV3 ==
종료 코드 0(으)로 완료된 프로세스
실행 결과 - BoundedQueueV3, 소비자 먼저 실행
23:14:23.445 [ main] == [소비자 먼저 실행] 시작, BoundedQueueV3 ==
23:14:23.474 [ main] 소비자 시작
23:14:23.493 [consumer1] [소비 시도] ? <- []
23:14:23.494 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
23:14:23.593 [consumer2] [소비 시도] ? <- []
23:14:23.594 [consumer2] [take] 큐에 데이터가 없음, 소비자 대기
23:14:23.703 [consumer3] [소비 시도] ? <- []
23:14:23.703 [consumer3] [take] 큐에 데이터가 없음, 소비자 대기
23:14:23.812 [ main] 현재 상태 출력, 큐 데이터: []
23:14:23.828 [ main] consumer1: WAITING
23:14:23.828 [ main] consumer2: WAITING
23:14:23.829 [ main] consumer3: WAITING
23:14:23.830 [ main] 생산자 시작
23:14:23.860 [producer1] [생산 시도] data1 -> []
23:14:23.863 [producer1] [put] 생산자 데이터 저장, notify() 호출
23:14:23.866 [producer1] [생산 완료] data1 -> [data1]
23:14:23.866 [consumer1] [take] 소비자 깨어남
23:14:23.867 [consumer1] [take] 소비자 데이터 획득, notify() 호출
23:14:23.869 [consumer2] [take] 소비자 깨어남
23:14:23.869 [consumer2] [take] 큐에 데이터가 없음, 소비자 대기
23:14:23.869 [consumer1] [소비 완료] data1 <- []
23:14:23.940 [producer2] [생산 시도] data2 -> []
23:14:23.941 [producer2] [put] 생산자 데이터 저장, notify() 호출
23:14:23.941 [producer2] [생산 완료] data2 -> [data2]
23:14:23.941 [consumer3] [take] 소비자 깨어남
23:14:23.943 [consumer3] [take] 소비자 데이터 획득, notify() 호출
23:14:23.945 [consumer3] [소비 완료] data2 <- []
23:14:23.945 [consumer2] [take] 소비자 깨어남
23:14:23.946 [consumer2] [take] 큐에 데이터가 없음, 소비자 대기
23:14:24.049 [producer3] [생산 시도] data3 -> []
23:14:24.050 [producer3] [put] 생산자 데이터 저장, notify() 호출
23:14:24.050 [consumer2] [take] 소비자 깨어남
23:14:24.051 [consumer2] [take] 소비자 데이터 획득, notify() 호출
23:14:24.050 [producer3] [생산 완료] data3 -> [data3]
23:14:24.051 [consumer2] [소비 완료] data3 <- []
23:14:24.161 [ main] 현재 상태 출력, 큐 데이터: []
23:14:24.166 [ main] consumer1: TERMINATED
23:14:24.167 [ main] consumer2: TERMINATED
23:14:24.168 [ main] consumer3: TERMINATED
23:14:24.168 [ main] producer1: TERMINATED
23:14:24.169 [ main] producer2: TERMINATED
23:14:24.169 [ main] producer3: TERMINATED
23:14:24.170 [ main] == [소비자 먼저 실행] 종료, BoundedQueueV3 ==
종료 코드 0(으)로 완료된 프로세스
참고: 모든 로그의 실행 순서는 각 환경마다 다를 수 있다.
Object - wait, notify - 예제3 분석 - 생산자 우선
BoundedQueueV3 - 생산자 먼저 실행 분석
실행 결과 - BoundedQueueV3, 생산자 먼저 실행
23:16:07.734 [ main] == [생산자 먼저 실행] 시작, BoundedQueueV3 ==
23:16:07.768 [ main] 생산자 시작
23:16:07.803 [producer1] [생산 시도] data1 -> []
23:16:07.803 [producer1] [put] 생산자 데이터 저장, notify() 호출
23:16:07.804 [producer1] [생산 완료] data1 -> [data1]
23:16:07.883 [producer2] [생산 시도] data2 -> [data1]
23:16:07.884 [producer2] [put] 생산자 데이터 저장, notify() 호출
23:16:07.884 [producer2] [생산 완료] data2 -> [data1, data2]
23:16:07.995 [producer3] [생산 시도] data3 -> [data1, data2]
23:16:07.996 [producer3] [put] 큐가 가득 참, 생산자 대기
23:16:08.112 [ main] 현재 상태 출력, 큐 데이터: [data1, data2]
23:16:08.128 [ main] producer1: TERMINATED
23:16:08.129 [ main] producer2: TERMINATED
23:16:08.130 [ main] producer3: WAITING
23:16:08.130 [ main] 소비자 시작
23:16:08.135 [consumer1] [소비 시도] ? <- [data1, data2]
23:16:08.135 [consumer1] [take] 소비자 데이터 획득, notify() 호출
23:16:08.135 [producer3] [put] 생산자 깨어남
23:16:08.136 [producer3] [put] 생산자 데이터 저장, notify() 호출
23:16:08.136 [producer3] [생산 완료] data3 -> [data2, data3]
23:16:08.137 [consumer1] [소비 완료] data1 <- [data2, data3]
23:16:08.248 [consumer2] [소비 시도] ? <- [data2, data3]
23:16:08.249 [consumer2] [take] 소비자 데이터 획득, notify() 호출
23:16:08.250 [consumer2] [소비 완료] data2 <- [data3]
23:16:08.360 [consumer3] [소비 시도] ? <- [data3]
23:16:08.361 [consumer3] [take] 소비자 데이터 획득, notify() 호출
23:16:08.361 [consumer3] [소비 완료] data3 <- []
23:16:08.471 [ main] 현재 상태 출력, 큐 데이터: []
23:16:08.472 [ main] producer1: TERMINATED
23:16:08.473 [ main] producer2: TERMINATED
23:16:08.473 [ main] producer3: TERMINATED
23:16:08.474 [ main] consumer1: TERMINATED
23:16:08.475 [ main] consumer2: TERMINATED
23:16:08.478 [ main] consumer3: TERMINATED
23:16:08.479 [ main] == [생산자 먼저 실행] 종료, BoundedQueueV3 ==
참고: 모든 로그의 실행 순서는 각 환경마다 다를 수 있다.
생산자 스레드 실행 시작

23:16:07.734 [ main] == [생산자 먼저 실행] 시작, BoundedQueueV3 ==
23:16:07.768 [ main] 생산자 시작
스레드 대기 집합(wait set)
- synchronized 임계 영역 안에서 Object.wait() 를 호출하면 스레드는 대기(WAITING) 상태에 들어간다. 이렇게 대기 상태에 들어간 스레드를 관리하는 것을 대기 집합 (wait set) 이라 한다. 참고로 모든 객체는 각자의 대기 집합을 가지고 있다.
- 모든 객체는 락(모니터 락) 과 대기 집합을 가지고 있다. 둘은 한쌍으로 사용된다. 따라서 락을 획득한 객체의 대기 집합을 사용해야 한다. 여기서 BoundedQueue(x001) 구현 인스턴스의 락과 대기집합을 사용한다.
- synchronized 를 메서드에 적용하면 해당 인스턴스의 락을 사용한다. 여기서는 BoundedQueue(x001) 의 구현체이다.
- wait() 호출은 앞에 this 를 생략할 수 있다. this 는 해당 인스턴스를 뜻한다. 여기서는 BoundedQueue(x001) 의 구현체이다.

23:16:07.803 [producer1] [생산 시도] data1 -> []
23:16:07.803 [producer1] [put] 생산자 데이터 저장, notify() 호출
p1 이 락을 획득하고 큐에 데이터를 저장한다.
큐에 데이터가 추가되었기 때문에 스레드 대기 집합에 이 사실을 알려야한다.
notify() 를 호출하면 스레드 대기 집합에서 대기하는 스레드 중 하나를 깨운다.
현재 대기 집합에 스레드가 없으므로 아무일도 발생하지 않는다. 만약 소비자 스레드가 대기집합에 있었다면 깨어나서 큐에 들어있는 데이터를 소비했을 것이다.

23:16:07.804 [producer1] [생산 완료] data1 -> [data1]

23:16:07.883 [producer2] [생산 시도] data2 -> [data1]
23:16:07.884 [producer2] [put] 생산자 데이터 저장, notify() 호출
23:16:07.884 [producer2] [생산 완료] data2 -> [data1, data2]
p2 도 큐에 데이터를 저장하고 생산을 완료한다.

23:16:07.995 [producer3] [생산 시도] data3 -> [data1, data2]
23:16:07.996 [producer3] [put] 큐가 가득 참, 생산자 대기
p3 가 데이터를 생산하려고 하는데, 큐가 가득 찼다. wait() 를 호출한다.
생산자 스레드 실행 완료

wait() 를 호출하면
락을 반납한다.
스레드의 상태가 RUNNABLE ->WAITINF 로 변경된다.
스레드 대기 집합에서 관리된다.
스레드 대기 집합에서 관리되는 스레드는 이후에 다른 스레드가 notify() 를 통해 스레드 대기집합에 신호를 주면 깨어날 수 있다.
23:16:08.112 [ main] 현재 상태 출력, 큐 데이터: [data1, data2]
23:16:08.128 [ main] producer1: TERMINATED
23:16:08.129 [ main] producer2: TERMINATED
23:16:08.130 [ main] producer3: WAITING
소비자 스레드 실행 시작

23:16:08.130 [ main] 소비자 시작

23:16:08.135 [consumer1] [소비 시도] ? <- [data1, data2]
23:16:08.135 [consumer1] [take] 소비자 데이터 획득, notify() 호출
- 소비자 스레드가 데이터를 획득했기 때문에 큐에 데이터를 보관할 빈 자리가 생겼다.
- 소비자 스레드는 notify() 를 호출해서 스레드 대기 집합에 이 사실을 알려준다.

23:16:08.135 [producer3] [put] 생산자 깨어남
23:16:08.136 [producer3] [put] 생산자 데이터 저장, notify() 호출
- 스레드 대기 집합은 notify() 신호를 받으면 대기 집합에 있는 스레드 중 하나를 깨운다.
- 그런데 대기 집합에 있는 스레드가 깨어난다고 바로 작동하는 것은 아니다. 깨어난 스레드는 여전히 임계영역 안에 있다.
- 임계 영역에 있는 코드를 실행하려면 먼저 락이 필요하다. p3는 대기 집합에서는 나가지만 여전히 임계 영역에 있으므로 락을 획득하기 위해 BLOCKED 상태로 대기한다. 당연한 이야기지만 임계 영역 안에서 2개의 스레드가 실행되면 큰 문제가 발생한다! 임계 영역 안에서는 락을 가지고 있는 하나의 스레드만 실행되어야 한다.
- p3 : WAITING -> BLOCKED
- 참고로 이때 임계 영역의 코드를 처름으로 돌아가서 실행하는 것은 아니다. 대기 집합에 들어오게 된 wait() 를 호출한 부분부터 실행된다. 락을 획득하면 wait() 이후의 코드를 실행한다.

23:16:08.137 [consumer1] [소비 완료] data1 <- [data2, data3]
c1 은 데이터 소비를 완료하고 락을 반납하고 임계영역을 빠져나간다.

23:16:08.135 [producer3] [put] 생산자 깨어남
23:16:08.136 [producer3] [put] 생산자 데이터 저장, notify() 호출
p3 가 락을 획득한다.
BLOCKED -> RUNNABLE
wait() 코드에서 대기했기 때문에 이후의 코드를 실행한다.
data3 을 큐에 저장한다.
notify()를 호출한다. 데이터를 저장했기 때문에 혹시 스레드 대기 집합에 소비자가 대기하고 있다면 소비자를 하나 깨워야한다. 물론 지금은 대기집합에 스레드가 없기 때문에 아무일도 일어나지 않는다.

23:16:08.136 [producer3] [생산 완료] data3 -> [data2, data3]

소비자 스레드 실행 완료

23:16:08.248 [consumer2] [소비 시도] ? <- [data2, data3]
23:16:08.249 [consumer2] [take] 소비자 데이터 획득, notify() 호출
23:16:08.250 [consumer2] [소비 완료] data2 <- [data3]
23:16:08.360 [consumer3] [소비 시도] ? <- [data3]
23:16:08.361 [consumer3] [take] 소비자 데이터 획득, notify() 호출
23:16:08.361 [consumer3] [소비 완료] data3 <- []
c2 , c3 를 실행한다. 데이터가 있으므로 둘다 데이터를 소비하고 완료한다.
둘다 notify() 를 호출하지만 대기집합에 스레드가 없으므로 아무일도 발생하지 않는다,
23:16:08.471 [ main] 현재 상태 출력, 큐 데이터: []
23:16:08.472 [ main] producer1: TERMINATED
23:16:08.473 [ main] producer2: TERMINATED
23:16:08.473 [ main] producer3: TERMINATED
23:16:08.474 [ main] consumer1: TERMINATED
23:16:08.475 [ main] consumer2: TERMINATED
23:16:08.478 [ main] consumer3: TERMINATED
23:16:08.479 [ main] == [생산자 먼저 실행] 종료, BoundedQueueV3 ==
정리
wait() , notify() 덕분에 스레드가 락을 놓고 대기하고, 또 대기하는 스레드를 필요한 시점에 깨울 수 있었다.
생산자 스레드가 큐가 가득차서 대기해도, 소비자 스레드가 큐의 데이터를 소비하고 나면 알려주기 때문에 최적의 타이밍에 꺠어나서 데이터를 생산할 수 있었다.
덕분에 최종 결과를 보면 p1, p2, p3 는 모두 데이터를 정상 생산하고, c1, c2, c3 는 모두 데이터를 정상 소비할 수 있었다.
다음에는 반대로 소비자를 먼저 실행해보자.
Object - wait, notify - 예제3 분석 - 소비자 우선
BoundedQueueV3 - 소비자 먼저 실행 분석
실행 결과 - BoundedQueueV3, 소비자 먼저 실행
00:58:19.677 [ main] == [소비자 먼저 실행] 시작, BoundedQueueV3 ==
00:58:19.719 [ main] 소비자 시작
00:58:19.742 [consumer1] [소비 시도] ? <- []
00:58:19.742 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
00:58:19.842 [consumer2] [소비 시도] ? <- []
00:58:19.843 [consumer2] [take] 큐에 데이터가 없음, 소비자 대기
00:58:19.954 [consumer3] [소비 시도] ? <- []
00:58:19.954 [consumer3] [take] 큐에 데이터가 없음, 소비자 대기
00:58:20.064 [ main] 현재 상태 출력, 큐 데이터: []
00:58:20.086 [ main] consumer1: WAITING
00:58:20.087 [ main] consumer2: WAITING
00:58:20.087 [ main] consumer3: WAITING
00:58:20.088 [ main] 생산자 시작
00:58:20.129 [producer1] [생산 시도] data1 -> []
00:58:20.129 [producer1] [put] 생산자 데이터 저장, notify() 호출
00:58:20.131 [producer1] [생산 완료] data1 -> [data1]
00:58:20.131 [consumer1] [take] 소비자 깨어남
00:58:20.133 [consumer1] [take] 소비자 데이터 획득, notify() 호출
00:58:20.133 [consumer2] [take] 소비자 깨어남
00:58:20.134 [consumer1] [소비 완료] data1 <- []
00:58:20.134 [consumer2] [take] 큐에 데이터가 없음, 소비자 대기
00:58:20.192 [producer2] [생산 시도] data2 -> []
00:58:20.194 [producer2] [put] 생산자 데이터 저장, notify() 호출
00:58:20.194 [consumer3] [take] 소비자 깨어남
00:58:20.195 [producer2] [생산 완료] data2 -> [data2]
00:58:20.195 [consumer3] [take] 소비자 데이터 획득, notify() 호출
00:58:20.196 [consumer3] [소비 완료] data2 <- []
00:58:20.196 [consumer2] [take] 소비자 깨어남
00:58:20.197 [consumer2] [take] 큐에 데이터가 없음, 소비자 대기
00:58:20.302 [producer3] [생산 시도] data3 -> []
00:58:20.303 [producer3] [put] 생산자 데이터 저장, notify() 호출
00:58:20.304 [consumer2] [take] 소비자 깨어남
00:58:20.305 [producer3] [생산 완료] data3 -> [data3]
00:58:20.305 [consumer2] [take] 소비자 데이터 획득, notify() 호출
00:58:20.306 [consumer2] [소비 완료] data3 <- []
00:58:20.412 [ main] 현재 상태 출력, 큐 데이터: []
00:58:20.413 [ main] consumer1: TERMINATED
00:58:20.413 [ main] consumer2: TERMINATED
00:58:20.414 [ main] consumer3: TERMINATED
00:58:20.415 [ main] producer1: TERMINATED
00:58:20.416 [ main] producer2: TERMINATED
00:58:20.417 [ main] producer3: TERMINATED
00:58:20.418 [ main] == [소비자 먼저 실행] 종료, BoundedQueueV3 ==
종료 코드 0(으)로 완료된 프로세스
참고: 모든 로그의 실행 순서는 각 환경마다 다를 수 있다. 그리고 여러분의 이해를 돕기 위해 실행시간이 같은 일 부 로그는 순서를 조정했다
소비자 스레드 실행 시작

00:58:19.677 [ main] == [소비자 먼저 실행] 시작, BoundedQueueV3 ==
00:58:19.719 [ main] 소비자 시작

00:58:19.742 [consumer1] [소비 시도] ? <- []
00:58:19.742 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기


소비자 스레드 실행 완료

00:58:19.719 [ main] 소비자 시작
00:58:19.742 [consumer1] [소비 시도] ? <- []
00:58:19.742 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
00:58:19.842 [consumer2] [소비 시도] ? <- []
00:58:19.843 [consumer2] [take] 큐에 데이터가 없음, 소비자 대기
00:58:19.954 [consumer3] [소비 시도] ? <- []
00:58:19.954 [consumer3] [take] 큐에 데이터가 없음, 소비자 대기
- 큐에 데이터가 없기 때문에 c1 , c2 , c3 모두 스레드 대기 집합에서 대기한다.
- 이후에 생산자가 큐에 데이터를 생산하면 notify() 를 통해 이 스레드들을 하나씩 깨워서 데이터를 소비할 수 있을 것이다.
00:58:20.064 [ main] 현재 상태 출력, 큐 데이터: []
00:58:20.086 [ main] consumer1: WAITING
00:58:20.087 [ main] consumer2: WAITING
00:58:20.087 [ main] consumer3: WAITING
생산자 스레드 실행 시작

00:58:20.088 [ main] 생산자 시작
00:58:20.129 [producer1] [생산 시도] data1 -> []
00:58:20.129 [producer1] [put] 생산자 데이터 저장, notify() 호출
p1 은 락을 획득하고, 큐에 데이터를 생산한다. 큐에 데이터가 있기 때문에 소비자를 하나 깨울 수 있다.
notify() 를 통해 스레드 대기 집합에 이 사실을 알려준다.

- notify()를 받은 스레드 대기 집합은 스레드 중에 하나를 깨운다.
- 여기서 c1, c2, c3 중에 어떤 스레드가 꺠어날까? ? 정답은 "예측할 수 없다"이다.
- 어떤 스레드가 깨워질지는 JVM 스펙에 명시되어 있지 않다. 따라서 JVM 버전 환경등에 따라서 달라진다
- 그런데 대기 집합에 있는 스레드가 깨어난다고 바로 작동하는 것은 아니다. 깨어난 스레드는 여전히 임계 영역안에 있다.
- 임계 영역에 있는 코드를 실행하려면 먼저 락이 필요하다. 대기 집합에서는 나가지만 여전히 임계 영역에 있으므로 락을 획득하기 위해 BLOCKED 상태로 대기한다.
- C1 : WAITING -> BLOCKED

00:58:20.131 [producer1] [생산 완료] data1 -> [data1]

00:58:20.131 [consumer1] [take] 소비자 깨어남
00:58:20.133 [consumer1] [take] 소비자 데이터 획득, notify() 호출
- c1 은 락을 획득하고, 임계 영역 안에 실행되며 데이터를 획득한다.
- c1 이 데이터를 획득했으므로 큐에 데이터를 넣을 공간이 있다는 것을 대기 집합에 알려준다. 만약 대기 집합에 생산자 스레드가 대기하고 있다면 큐에 데이터를 넣을 수 있을것이다.

c1 이 notify() 로 스레드 대기 집합에 알렸지만, 생산자 스레드가 아니라 소비자 스레드만 있다. 따라서 의도와는 다르게 소비자 스레드인 c2 가 대기상태에서 깨어난다. (물론 대기 집합에 있는 어떤 스레드가 깨어날지는 알 수 없다. 여기서는 c2 가 깨어난다고 가정한다. 심지어 생산자와 소비자 스레드가 함께 대기 집합에 있어도 어 떤 스레드가 깨어날지는 알 수 없다. )

00:58:20.134 [consumer1] [소비 완료] data1 <- []
c1은 작업을 완료했다.
c1 이 c2 를 깨웠지만, 문제가 하나 있다. 바로 큐에 데이터가 없다는 점이다.

00:58:20.133 [consumer2] [take] 소비자 깨어남
00:58:20.134 [consumer2] [take] 큐에 데이터가 없음, 소비자 대기
c2 는 락을 획득하고 , 큐에 데이터를 소비하려고 시도한다. 그런데 큐에는 데이터가 없다.
큐에 데이터가 없기 떄문에 , c2 는 결국 wait() 를 호출해서 대기 상태로 변하며 다시 대기 집합으로 들어간다.

- 이처럼 소비자인 c1 이 같은 소비자인 c2 를 깨우는 것은 상당히 비효율적이다.
- c1 입장에서 c2 를 깨우게 되면 아무 일도 하지 않고 그냥 다시 스레드 대기 집합에 들어갈 수 있다. 결과적으로 CPU 만 사용하고 , 아무일도 하지 않는 상태로 다시 대기상태가 되어 버린다.
- 그렇다고 c1 이 스레드 대기 집합에 있는 어떤 스레드를 꺠울지 선택할 수 없다. notify() 는 스레드 대기 집합에 있는 스레드중 임의의 하나를 깨울 뿐이다.
- 물론 이것이 비효율적이라는 것이지 문제가 되는 것은 아니다. 결과에는 문제가 없다. 가끔씩 약간 돌아서 갈 뿐이다


00:58:20.192 [producer2] [생산 시도] data2 -> []
00:58:20.194 [producer2] [put] 생산자 데이터 저장, notify() 호출
p2 가 락을 획득하고 데이터를 저장한 다음에 notify() 를 호출한다. 데이터가 있으므로 소비자 스레드가 깨어난다면 데이터를 소비할 수 있다.

스레드 대기 집합에 있는 c3 가 깨어난다. 참고로 어떤 스레드가 깨어날 지는 알 수 없다.
c3 는 임계 영역 안에 있으므로 락을 획득하기 위해 대기(BLOCKED) 한다.

00:58:20.195 [producer2] [생산 완료] data2 -> [data2]

00:58:20.194 [consumer3] [take] 소비자 깨어남
00:58:20.195 [consumer3] [take] 소비자 데이터 획득, notify() 호출
c3 는 락을 획득하고 BLOCKED RUNNABLE 상태가 된다.
c3 는 데이터를 획득한 다음에 notify() 를 통해 스레드 대기 집합에 알린다. 큐에 여유 공간이 생겼기 때문에 생산자 스레드가 대기 중이라면 데이터를 생산할 수 있다.

- 생산자 스레드를 깨울 것으로 기대하고, notify() 를 호출했지만 스레드 대기 집합에는 소비자인 c2 만 존재한다.
- c2 가 깨어나지만 임계 영역 안에 있으므로 락을 기다리는 BLOCKED 상태가 된다.

00:58:20.196 [consumer3] [소비 완료] data2 <- []

00:58:20.196 [consumer2] [take] 소비자 깨어남
00:58:20.197 [consumer2] [take] 큐에 데이터가 없음, 소비자 대기
- c2 가 락을 획득하고, 큐에서 데이터를 획득하려 하지만 데이터가 없다.
- c2 는 다시 wait() 를 호출해서 대기( WAITING ) 상태에 들어가고, 다시 대기 집합에서 관리된다.

- 물론 c2 의 지금 이 사이클은 CPU 자원만 소모하고 다시 대기 집합에 들어갔기 때문에 비효율적이다.
- 만약 소비자인 c3 입장에서 생산자, 소비자 스레드를 선택해서 깨울 수 있다면, 소비자인 c2 를 깨우지는 않았을 것이다. 하지만 notify() 는 이런 선택을 할 수 없다.


00:58:20.302 [producer3] [생산 시도] data3 -> []
00:58:20.303 [producer3] [put] 생산자 데이터 저장, notify() 호출
- p3 가 데이터를 저장하고 notify() 를 통해 스레드 대기 집합에 알린다.
- 스레드 대기 집합에는 소비자 c2 가 있으므로 생산한 데이터를 잘 소비할 수 있다.


00:58:20.305 [producer3] [생산 완료] data3 -> [data3]

00:58:20.304 [consumer2] [take] 소비자 깨어남
00:58:20.305 [consumer2] [take] 소비자 데이터 획득, notify() 호출

00:58:20.306 [consumer2] [소비 완료] data3 <- []

00:58:20.412 [ main] 현재 상태 출력, 큐 데이터: []
00:58:20.413 [ main] consumer1: TERMINATED
00:58:20.413 [ main] consumer2: TERMINATED
00:58:20.414 [ main] consumer3: TERMINATED
00:58:20.415 [ main] producer1: TERMINATED
00:58:20.416 [ main] producer2: TERMINATED
00:58:20.417 [ main] producer3: TERMINATED
00:58:20.418 [ main] == [소비자 먼저 실행] 종료, BoundedQueueV3 ==
정리
최종 결과를 보면 p1, p2 , p3 는 모두 데이터를 정상 생성하고 c1, c2, c3 는 모두 데이터를 정상 소비할 수 있었다. 하지만 소비자인 c1 이 같은 소비자인 c2, c3 를 꺠울 수 있었다. 이 경우 큐에 데이터가 없을 가능성이 있다. 이때는 꺠어난 소비자 스레드가 CPU 자원만 소모하고 다시 대기 집합으로 들어갔기 때문에 비효율적이다.
만약 소비자인 c1 입장에서 생산자, 소비자 스레드를 선택해서 깨울 수 있다면, 소비자인 c2 를 깨우지는 않았을 것이 다. 예를 들어서 소비자는 생산자만 깨우고, 생산자는 소비자만 깨울 수 있다면 더 효율적으로 작동할 수 있을 것 같다. 하지 만 notify() 는 이런 선택을 할 수 없다. 물론 이것이 비효율적이라는 것이지 결과에는 아무런 문제가 없다. 약간 돌아서 갈 뿐이다.
Object - wait, notify - 한계
지금까지 살펴본 Object.wait() , Object.notify() 방식은 스레드 대기 집합 하나에 생산자, 소비자 스레드를 모두 관리한다. 그리고 notify() 를 호출할 때 임의의 스레드가 선택된다. 따라서 앞서 살펴본 것 처럼 큐에 데이터가 없는 상황에 소비자가 같은 소비자를 깨우는 비효율이 발생할 수 있다. 또는 큐에 데이터가 가득 차있는데 생산자가 같 은 생산자를 깨우는 비효율도 발생할 수 있다.
이 문제를 다시 한번 정리해보자.
비효율 - 생산자 실행 예시

다음과 같은 상황을 가정하겠다.
- 큐에 dataX 가 보관되어 있다.
- 스레드 대기 집합에는 다음 스레드가 대기하고 있다.
- 소비자: c1 , c2 , c3
- 생산자: p1 , p2 , p3
- p0 스레드가 data0 생산을 시도한다.

p0 스레드가 실행되면서 data0 를 큐에 저장한다.
이때 큐에 데이터가 가득 찬다. notify() 를 통해 대기 집합의 스레드를 하나 깨운다.
만약 notify() 의 결과로 소비자 스레드가 깨어나게 되면 소비자 스레드는 큐의 데이터를 획득하고, 완료된다.

만약 notify() 의 결과로 소비자 스레드가 깨어나게 되면 소비자 스레드는 큐의 데이터를 획득하고, 완료된다

만약 notify() 의 결과로 생산자 스레드를 깨우게 되면, 이미 큐에 데이터는 가득 차 있다. 따라서 데이터를 생 산하지 못하고 다시 대기 집합으로 이동하는 비효율이 발생한다.
비효율 - 소비자 실행 예시

이번에는 반대의 경우로 소비자 c0 를 실행해보자.

- c0 스레드가 실행되고 data0 를 획득한다.
- 이제 큐에 데이터는 비어있게 된다.
- c0 스레드는 notify() 를 호출한다.

스레드 대기 집합에서 소비자 스레드가 깨어나면 큐에 데이터가 없기 때문에 다시 대기 집합으로 이동하는 비효율 이 발생한다.

스레드 대기 집합에서 생산자 스레드가 깨어나면 큐에 데이터를 저장하고 완료된다.
같은 종류의 스레드를 깨울 때 비효율이 발생한다.
이 내용을 통해서 알 수 있는 사실은 생산자가 같은 생산자를 깨우거나, 소비자가 같은 소비자를 깨울 때 비효율이 발생 할 수 있다는 점이다. 생산자가 소비자를 깨우고, 반대로 소비자가 생산자를 깨운다면 이런 비효율은 발생하지 않는다.
스레드 기아(thread starvation) notify() 의 또 다른 문제점으로는 어떤 스레드가 깨어날 지 알 수 없기 때문에 발생할 수 있는 스레드 기아 문제가 있다
'JAVA' 카테고리의 다른 글
Java에서의 지연 초기화,지연 평가 Optional 활용: orElse vs orElseGet (0) | 2025.01.06 |
---|---|
고급 동기화 - concurrent.Lock (1) | 2025.01.05 |
생산자 소비자 문제1 (1) | 2024.12.31 |
멀티스레드 환경에서 volatile 의 중요성: 메모리 가시성 문제 해결 (0) | 2024.12.29 |
익명 클래스 (1) | 2024.12.24 |