8. 생산자 소비자 문제1

임대일·2025년 5월 13일

Thread

목록 보기
8/13
post-thumbnail

1. 생산자 소비자 문제 소개 및 예제1 - BoundedQueueV1

생산자-소비자 문제란?

  • 멀티스레드 환경에서 자원을 공유하는 생산자(Producer)와 소비자(Consumer)가 충돌 없이 데이터를 처리해야 하는 동기화 문제입니다.
  • 한정된 버퍼(Bounded Buffer)를 매개로 데이터를 주고받으며 발생하는 충돌과 대기 상황을 다룹니다.

문제 핵심

  • 생산자가 너무 빠르면 → 버퍼가 가득 차고, 데이터를 더 넣지 못합니다.
  • 소비자가 너무 빠르면 → 버퍼가 비어 있고, 소비할 데이터가 없습니다.

비유로 이해하기

레스토랑 모델

  • 생산자: 요리사
  • 소비자: 손님
  • 버퍼: 서빙 테이블
  • 테이블이 가득 차면 요리사는 기다려야 하고, 비어 있으면 손님은 기다려야 하빈다.

음료 공장 모델

  • 생산자: 공장
  • 소비자: 상점
  • 버퍼: 창고

예제1 구조 (BoundedQueueV1)

인터페이스

package thread.bounded;

public interface BoundedQueue {
  void put(String data); // 생산자 호출
  String take();  // 소비자 호출
}

구현 클래스 (V1)

package thread.bounded;

import static util.MyLogger.log;

import java.util.ArrayDeque;
import java.util.Queue;

public class BoundedQueueV1 implements BoundedQueue {

  private final Queue<String> queue = new ArrayDeque<>();
  private final int max;

  public BoundedQueueV1(int max) {
    this.max = max;
  }

  @Override
  public synchronized void put(String data) {
    if (queue.size() == max) {
      log("[put] 큐가 가득 참, 버림: " + data);
      return;
    }
    queue.offer(data);
  }

  @Override
  public synchronized String take() {
    if (queue.isEmpty()) {
      return null;
    }
    return queue.poll();
  }

  @Override
  public String toString() {
    return queue.toString();
  }
}
  • 공유 자원: queue (ArrayDeque)
  • synchronized를 이용해 임계 영역 보호합니다.
  • 단, 큐가 가득 차거나 비었을 경우 단순히 버리거나 null 반환됩니다.

생산자/소비자 스레드 구현 클래스

package thread.bounded;

import static util.MyLogger.log;

public class ConsumerTask implements Runnable {

  private BoundedQueue queue;

  public ConsumerTask(BoundedQueue queue) {
    this.queue = queue;
  }

  @Override
  public void run() {
    log("[소비 시도]     ? <- " + queue);
    String data = queue.take();
    log("[소비 완료] " +  data + " <- " + queue);
  }
}
package thread.bounded;

import static util.MyLogger.log;

public class ProducerTask implements Runnable {

  private BoundedQueue queue;
  private String request;

  public ProducerTask(BoundedQueue queue, String request) {
    this.queue = queue;
    this.request = request;
  }

  @Override
  public void run() {
    log("[생산 시도] " + request + " -> " + queue);
    queue.put(request);
    log("[생산 완료] " + request + " -> " + queue);
  }
}

실행 코드 요약 (BoundedMain)

package thread.bounded;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

import java.util.ArrayList;
import java.util.List;

public class BoundedMain {
  public static void main(String[] args) {
    // 1. BoundedQueue 선택
    BoundedQueue queue = new BoundedQueueV1(2);
//    BoundedQueue queue = new BoundedQueueV2(2);
//    BoundedQueue queue = new BoundedQueueV3(2);
//    BoundedQueue queue = new BoundedQueueV4(2);
//    BoundedQueue queue = new BoundedQueueV5(2);
//    BoundedQueue queue = new BoundedQueueV6_1(2);
//    BoundedQueue queue = new BoundedQueueV6_2(2);
//    BoundedQueue queue = new BoundedQueueV6_3(2);
//    BoundedQueue queue = new BoundedQueueV6_4(2);

    // 2. 생산자, 소비자 실행 순서 선택, 반드시 하나만 선택!
    producerFirst(queue);
//    consumerFirst(queue);

  }

  private static void producerFirst(BoundedQueue queue) {
    log("== [생산자 먼저 실행] 시작, " + queue.getClass().getSimpleName() + " ==");
    List<Thread> threads = new ArrayList<>();
    startProducer(queue, threads);
    printAllState(queue, threads);
    startConsumer(queue, threads);
    printAllState(queue, threads);
    log("== [생산자 먼저 실행] 종료, " + queue.getClass().getSimpleName() + " ==");
  }

  private static void consumerFirst(BoundedQueue queue) {
    log("== [소비자 먼저 실행] 시작, " + queue.getClass().getSimpleName() + " ==");
    List<Thread> threads = new ArrayList<>();
    startConsumer(queue, threads);
    printAllState(queue, threads);
    startProducer(queue, threads);
    printAllState(queue, threads);
    log("== [소비자 먼저 실행] 종료, " + queue.getClass().getSimpleName() + " ==");
  }

  private static void printAllState(BoundedQueue queue, List<Thread> threads) {
    System.out.println();
    log("현재 상태 출력, 큐 데이터: " + queue);
    for (Thread thread : threads) {
      log(thread.getName() + ": " + thread.getState());
    }
  }

  private static void startProducer(BoundedQueue queue, List<Thread> threads) {
    System.out.println();
    log("생산자 시작");
    for (int i = 1; i <= 3; i++) {
      Thread producer = new Thread(new ProducerTask(queue, "data" + i), "producer" + i);
      threads.add(producer);
      producer.start();
      sleep(100);
    }
  }

  private static void startConsumer(BoundedQueue queue, List<Thread> threads) {
    System.out.println();
    log("소비자 시작");
    for (int i = 1; i <= 3; i++) {
      Thread consumer = new Thread(new ConsumerTask(queue), "consumer" + i);
      threads.add(consumer);
      consumer.start();
      sleep(100);
    }
  }
}
  • 생산자 먼저 실행 / 소비자 먼저 실행 중 하나만 선택 실행합니다.
  • producerFirst():
    1. 생산자 3개 실행
    2. 소비자 3개 실행
  • consumerFirst()는 반대 순서로 동작합니다.
15:08:44.306 [     main] == [생산자 먼저 실행] 시작, BoundedQueueV1 ==
15:08:44.308 [     main] 생산자 시작
15:08:44.313 [producer1] [생산 시도] data1 -> []
15:08:44.313 [producer1] [생산 완료] data1 -> [data1]
15:08:44.415 [producer2] [생산 시도] data2 -> [data1]
15:08:44.415 [producer2] [생산 완료] data2 -> [data1, data2]
15:08:44.520 [producer3] [생산 시도] data3 -> [data1, data2]
15:08:44.521 [producer3] [put] 큐가 가득 참, 버림: data3
15:08:44.521 [producer3] [생산 완료] data3 -> [data1, data2]
  
15:08:44.625 [     main] 현재 상태 출력, 큐 데이터: [data1, data2]
15:08:44.626 [     main] producer1: TERMINATED
15:08:44.626 [     main] producer2: TERMINATED
15:08:44.626 [     main] producer3: TERMINATED
  
15:08:44.626 [     main] 소비자 시작
15:08:44.627 [consumer1] [소비 시도]     ? <- [data1, data2]
15:08:44.627 [consumer1] [소비 완료] data1 <- [data2]
15:08:44.730 [consumer2] [소비 시도]     ? <- [data2]
15:08:44.730 [consumer2] [소비 완료] data2 <- []
15:08:44.835 [consumer3] [소비 시도]     ? <- []
15:08:44.836 [consumer3] [소비 완료] null <- []
  
15:08:44.941 [     main] 현재 상태 출력, 큐 데이터: []
15:08:44.941 [     main] producer1: TERMINATED
15:08:44.941 [     main] producer2: TERMINATED
15:08:44.941 [     main] producer3: TERMINATED
15:08:44.941 [     main] consumer1: TERMINATED
15:08:44.941 [     main] consumer2: TERMINATED
15:08:44.942 [     main] consumer3: TERMINATED
15:08:44.942 [     main] == [생산자 먼저 실행] 종료, BoundedQueueV1 ==

결과 분석 요약

생산자 먼저 실행

  • producer1, 2 → 정상 저장
  • producer3 → 큐가 가득 차 data3 버림
  • consumer1, 2 → 정상 소비
  • consumer3 → 큐가 비어 null 반환

소비자 먼저 실행

  • consumer1, 2, 3 → 모두 큐가 비어 null 반환
  • producer1, 2 → 정상 저장
  • producer3 → 큐가 가득 차 data3 버림

문제점

  • data3소비되지 못하고 버려집니다.
  • consumer3데이터를 못 받아 null 처리됩니다.

2. 예제2 - BoundedQueueV2: Thread.sleep()으로 대기 구현

목적

  • 예제1에서는 큐가 가득 차거나 비어 있으면 그냥 버리거나 null 반환
  • 이번에는 "기다렸다가 다시 시도"하는 구조로 개선

개선 코드 요약

package thread.bounded;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

import java.util.ArrayDeque;
import java.util.Queue;

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();
  }
}
13:45:00.496 [     main] == [생산자 먼저 실행] 시작, BoundedQueueV2 ==

13:45:00.497 [     main] 생산자 시작
13:45:00.506 [producer1] [생산 시도] data1 -> []
13:45:00.506 [producer1] [생산 완료] data1 -> [data1]
13:45:00.613 [producer2] [생산 시도] data2 -> [data1]
13:45:00.614 [producer2] [생산 완료] data2 -> [data1, data2]
13:45:00.719 [producer3] [생산 시도] data3 -> [data1, data2]
13:45:00.720 [producer3] [put] 큐가 가득 참, 생산자 대기

13:45:00.825 [     main] 현재 상태 출력, 큐 데이터: [data1, data2]
13:45:00.826 [     main] producer1: TERMINATED
13:45:00.826 [     main] producer2: TERMINATED
13:45:00.826 [     main] producer3: TIMED_WAITING

13:45:00.826 [     main] 소비자 시작
13:45:00.828 [consumer1] [소비 시도]     ? <- [data1, data2]
13:45:00.930 [consumer2] [소비 시도]     ? <- [data1, data2]
13:45:01.031 [consumer3] [소비 시도]     ? <- [data1, data2]

13:45:01.133 [     main] 현재 상태 출력, 큐 데이터: [data1, data2]
13:45:01.133 [     main] producer1: TERMINATED
13:45:01.133 [     main] producer2: TERMINATED
13:45:01.133 [     main] producer3: TIMED_WAITING
13:45:01.133 [     main] consumer1: BLOCKED
13:45:01.133 [     main] consumer2: BLOCKED
13:45:01.134 [     main] consumer3: BLOCKED
13:45:01.134 [     main] == [생산자 먼저 실행] 종료, BoundedQueueV2 ==
  
13:45:01.720 [producer3] [put] 큐가 가득 참, 생산자 대기
13:45:02.724 [producer3] [put] 큐가 가득 참, 생산자 대기
13:45:03.726 [producer3] [put] 큐가 가득 참, 생산자 대기
...
13:46:30.475 [     main] == [소비자 먼저 실행] 시작, BoundedQueueV2 ==

13:46:30.476 [     main] 소비자 시작
13:46:30.479 [consumer1] [소비 시도]     ? <- []
13:46:30.479 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
13:46:30.589 [consumer2] [소비 시도]     ? <- []
13:46:30.695 [consumer3] [소비 시도]     ? <- []

13:46:30.795 [     main] 현재 상태 출력, 큐 데이터: []
13:46:30.796 [     main] consumer1: TIMED_WAITING
13:46:30.796 [     main] consumer2: BLOCKED
13:46:30.796 [     main] consumer3: BLOCKED

13:46:30.796 [     main] 생산자 시작
13:46:30.798 [producer1] [생산 시도] data1 -> []
13:46:30.900 [producer2] [생산 시도] data2 -> []
13:46:31.001 [producer3] [생산 시도] data3 -> []

13:46:31.101 [     main] 현재 상태 출력, 큐 데이터: []
13:46:31.101 [     main] consumer1: TIMED_WAITING
13:46:31.101 [     main] consumer2: BLOCKED
13:46:31.102 [     main] consumer3: BLOCKED
13:46:31.102 [     main] producer1: BLOCKED
13:46:31.102 [     main] producer2: BLOCKED
13:46:31.102 [     main] producer3: BLOCKED
13:46:31.103 [     main] == [소비자 먼저 실행] 종료, BoundedQueueV2 ==
13:46:31.481 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
13:46:32.487 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
13:46:33.488 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
13:46:34.489 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
...

핵심 문제

synchronized + sleep()의 조합

  • sleep()스레드를 일시 정지시킬 뿐, 락을 반납하지 않습니다.
  • 따라서 queue의 락을 잠근 채로 잠들어 있습니다.
  • 다른 스레드는 해당 락에 접근할 수 없기 때문에, 대기 중인 스레드도 계속 깨어나지 못함 → 교착에 가까운 무한 대기

실행 결과

  • 생산자 먼저 실행:
    • producer1, 2 → 정상 저장합니다.
    • producer3queue.size() == max로 sleep 진입합니다.
    • 락을 계속 잡고 있어서 이후 소비자는 take() 메서드 호출조차 못하게 됩니다. → 교착 상태 발생
  • 소비자 먼저 실행 시도:
    • queue.isEmpty()로 sleep 진입합니다. → 락 보유한 채 잠듦
    • 이후 생산자는 put()에 진입을 못하게 됩니다. → 역시 교착 상태 발생

요약 정리

항목설명
개선 의도큐가 꽉 찼거나 비어 있으면 기다렸다가 다시 시도
사용 방식synchronized 내부에서 Thread.sleep() 호출
문제점락을 점유한 채로 잠듦 → 다른 스레드가 진입 못 함
결과무한 대기 및 교착 상태 발생 가능
결론sleep()은 락을 놓지 않기 때문에, 동기화 문제 해결에는 부적절

스레드 간 협력이 필요할 때는 sleep()이 아니라 wait() / notify()처럼 락을 반납하는 방식의 동기화 메커니즘을 고려할 수 있습니다.

3. 예제3 - BoundedQueueV3: wait() / notify()로 스레드 간 협력 구현

개선 목적

  • 이전 버전(V2)의 sleep() 방식은 락을 점유한 채로 대기하여 다른 스레드 진입 자체가 불가능으로 더 심각한 문제였습니다.
  • 이제는 스레드가 락을 자발적으로 내려놓고(wait), 다른 스레드가 이를 깨우는(notify) 구조로 개선합니다.

핵심 메커니즘

  • wait():
    • 현재 스레드는 해당 객체의 락을 반납하고 WAITING 상태 진입합니다.
    • 누군가 notify()로 깨워줄 때까지 잠듭니다.
  • notify():
    • 같은 객체에 대해 wait() 중인 스레드 중 하나를 깨웁니다.
    • 깨어난 스레드는 다시 락을 획득하면 실행 재개합니다.

개선 코드 요약 (V3)

package thread.bounded;

import static util.MyLogger.log;

import java.util.ArrayDeque;
import java.util.Queue;

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
    // notifyAll(); // 모든 대기 스레드, 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
    // notifyAll(); // 모든 대기 스레드, WAIT -> BLOCKED
    return data;
  }

  @Override
  public String toString() {
    return queue.toString();
  }
}

주요 변경점 요약

변경점설명
while 사용깨워져도 조건이 다시 false일 수 있음 → 조건 재검사 필수
wait() 사용대기하면서 락을 반납 → 다른 스레드 진입 가능
notify() 사용작업 후 다른 쪽 스레드를 깨움

실행 흐름: 생산자 먼저 실행

14:18:25.544 [     main] == [생산자 먼저 실행] 시작, BoundedQueueV3 ==

14:18:25.546 [     main] 생산자 시작
14:18:25.554 [producer1] [생산 시도] data1 -> []
14:18:25.554 [producer1] [put] 생산자 데이터 저장, notify() 호출
14:18:25.555 [producer1] [생산 완료] data1 -> [data1]
14:18:25.652 [producer2] [생산 시도] data2 -> [data1]
14:18:25.653 [producer2] [put] 생산자 데이터 저장, notify() 호출
14:18:25.653 [producer2] [생산 완료] data2 -> [data1, data2]
14:18:25.757 [producer3] [생산 시도] data3 -> [data1, data2]
14:18:25.757 [producer3] [put] 큐가 가득 참, 생산자 대기

14:18:25.863 [     main] 현재 상태 출력, 큐 데이터: [data1, data2]
14:18:25.864 [     main] producer1: TERMINATED
14:18:25.864 [     main] producer2: TERMINATED
14:18:25.864 [     main] producer3: WAITING

14:18:25.864 [     main] 소비자 시작
14:18:25.865 [consumer1] [소비 시도]     ? <- [data1, data2]
14:18:25.866 [consumer1] [take] 소비자 데이터 획득, notify() 호출
14:18:25.866 [producer3] [put] 생산자 깨어남
14:18:25.866 [consumer1] [소비 완료] data1 <- [data2]
14:18:25.866 [producer3] [put] 생산자 데이터 저장, notify() 호출
14:18:25.866 [producer3] [생산 완료] data3 -> [data2, data3]
14:18:25.968 [consumer2] [소비 시도]     ? <- [data2, data3]
14:18:25.968 [consumer2] [take] 소비자 데이터 획득, notify() 호출
14:18:25.968 [consumer2] [소비 완료] data2 <- [data3]
14:18:26.073 [consumer3] [소비 시도]     ? <- [data3]
14:18:26.073 [consumer3] [take] 소비자 데이터 획득, notify() 호출
14:18:26.074 [consumer3] [소비 완료] data3 <- []

14:18:26.180 [     main] 현재 상태 출력, 큐 데이터: []
14:18:26.180 [     main] producer1: TERMINATED
14:18:26.181 [     main] producer2: TERMINATED
14:18:26.181 [     main] producer3: TERMINATED
14:18:26.181 [     main] consumer1: TERMINATED
14:18:26.182 [     main] consumer2: TERMINATED
14:18:26.182 [     main] consumer3: TERMINATED
14:18:26.183 [     main] == [생산자 먼저 실행] 종료, BoundedQueueV3 ==
  1. producer1, 2 → 정상 저장
  2. producer3 → 큐 가득 참 → wait()로 대기 (락 반납)
  3. consumer1, 2 → 소비 진행 → 큐 공간 생김 → notify()producer3 깨움
  4. producer3 → 다시 락 획득 후 정상 저장

실행 흐름: 소비자 먼저 실행

14:17:09.897 [     main] == [소비자 먼저 실행] 시작, BoundedQueueV3 ==

14:17:09.899 [     main] 소비자 시작
14:17:09.903 [consumer1] [소비 시도]     ? <- []
14:17:09.903 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
14:17:10.011 [consumer2] [소비 시도]     ? <- []
14:17:10.012 [consumer2] [take] 큐에 데이터가 없음, 소비자 대기
14:17:10.116 [consumer3] [소비 시도]     ? <- []
14:17:10.117 [consumer3] [take] 큐에 데이터가 없음, 소비자 대기

14:17:10.221 [     main] 현재 상태 출력, 큐 데이터: []
14:17:10.222 [     main] consumer1: WAITING
14:17:10.222 [     main] consumer2: WAITING
14:17:10.222 [     main] consumer3: WAITING

14:17:10.222 [     main] 생산자 시작
14:17:10.225 [producer1] [생산 시도] data1 -> []
14:17:10.225 [producer1] [put] 생산자 데이터 저장, notify() 호출
14:17:10.225 [consumer1] [take] 소비자 꺠어남
14:17:10.225 [producer1] [생산 완료] data1 -> [data1]
14:17:10.225 [consumer1] [take] 소비자 데이터 획득, notify() 호출
14:17:10.226 [consumer2] [take] 소비자 꺠어남
14:17:10.226 [consumer2] [take] 큐에 데이터가 없음, 소비자 대기
14:17:10.226 [consumer1] [소비 완료] data1 <- []
14:17:10.327 [producer2] [생산 시도] data2 -> []
14:17:10.327 [producer2] [put] 생산자 데이터 저장, notify() 호출
14:17:10.327 [producer2] [생산 완료] data2 -> [data2]
14:17:10.327 [consumer3] [take] 소비자 꺠어남
14:17:10.328 [consumer3] [take] 소비자 데이터 획득, notify() 호출
14:17:10.328 [consumer3] [소비 완료] data2 <- []
14:17:10.328 [consumer2] [take] 소비자 꺠어남
14:17:10.328 [consumer2] [take] 큐에 데이터가 없음, 소비자 대기
14:17:10.433 [producer3] [생산 시도] data3 -> []
14:17:10.433 [producer3] [put] 생산자 데이터 저장, notify() 호출
14:17:10.433 [consumer2] [take] 소비자 꺠어남
14:17:10.433 [producer3] [생산 완료] data3 -> [data3]
14:17:10.434 [consumer2] [take] 소비자 데이터 획득, notify() 호출
14:17:10.434 [consumer2] [소비 완료] data3 <- []

14:17:10.539 [     main] 현재 상태 출력, 큐 데이터: []
14:17:10.539 [     main] consumer1: TERMINATED
14:17:10.540 [     main] consumer2: TERMINATED
14:17:10.540 [     main] consumer3: TERMINATED
14:17:10.540 [     main] producer1: TERMINATED
14:17:10.540 [     main] producer2: TERMINATED
14:17:10.540 [     main] producer3: TERMINATED
14:17:10.541 [     main] == [소비자 먼저 실행] 종료, BoundedQueueV3 ==
  1. consumer1, 2, 3 → 큐 비어있음 → wait()로 대기
  2. producer1, 2, 3 → 생산하고 notify() → 소비자 깨어나서 소비 진행

개선 효과

항목예제2(V2)예제3(V3)
대기 방식sleep() (비효율, 락 점유)wait() (효율적, 락 반납)
락 점유유지 → 교착 위험반납 → 진입 가능
반응성느림 (시간 기반 대기)빠름 (상태 기반 협력)
협업 구조없음있음 (waitnotify)

핵심 정리

  • wait()notify()멀티스레드 협력에서 가장 기본이 되는 메시징 방식입니다.
  • sleep()은 단독 대기, wait()상호 협력 구조입니다.

4. 예제4 - BoundedQueueV4: notify()notifyAll() 개선

개선 필요성: notify()의 한계

  • 예제3에서는 생산자 또는 소비자 하나만 깨우기 위해 notify()를 사용합니다.
  • 하지만 스레드가 여러 명인 경우 문제가 발생할 수 있습니다.
  • 최악의 경우는 대기 상태의 스레드가 실행 순서를 계속 얻지 못해서 실행되지 않는 상황인 스레드 기아(starvation) 상태가 발생할 수 있습니다.

문제 상황 (예: 소비자만 여러 명)

if (queue.isEmpty()) {
    wait(); // 여러 소비자 대기
}
// ...
notify(); // 소비자 중 하나를 깨움 → 여전히 큐는 비어 있음 → 다시 wait
  • 깨운 스레드가 조건을 만족하지 못하는 경우
    바로 다시 wait
    전체 시스템이 교착 상태에 빠질 수 있음

해결 방법: notifyAll()

  • 모든 대기 중인 스레드를 깨웁니다. → 조건을 만족하는 스레드만 실행
  • 조건을 만족하지 않는 스레드는 다시 wait로 돌아갑니다.
  • 조건을 만족하는 스레드만 살아남아 진행합니다.

코드

package thread.bounded;

import static util.MyLogger.log;

import java.util.ArrayDeque;
import java.util.Queue;

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
     notifyAll(); // 모든 대기 스레드, 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
     notifyAll(); // 모든 대기 스레드, WAIT -> BLOCKED
    return data;
  }

  @Override
  public String toString() {
    return queue.toString();
  }
}

notifyAll() vs notify()

항목notify()notifyAll()
대상대기 중인 스레드 1개모든 대기 중인 스레드
위험조건 미충족한 스레드가 깨어날 수 있음 → 무한 대기 위험다 깨어나서 조건 확인 후 필요한 스레드만 진행
권장단일 생산자/소비자다중 생산자/소비자 환경에서는 필수

while문으로 감싸는 이유 복습

while (조건) {
    wait();
}
  • wait()에서 깨어나도 조건이 참이라는 보장이 없습니다.
  • notify()/notifyAll()은 단지 신호일 뿐 → 스레드는 조건 재확인은 필수입니다.

예제4의 개선 요약

항목설명
문제notify()는 조건 미충족한 스레드를 깨울 수 있음
증상깨운 스레드가 다시 wait()진전 없음
해결notifyAll()모두 깨운 후 조건 확인
추가 장치while 루프로 조건 재확인 → false면 다시 wait()

핵심 정리

  • wait()락을 반납하고 대기합니다.
  • notify()랜덤하게 하나만 깨웁니다. → 다중 스레드 환경에선 불안정합니다.
  • notifyAll()모든 대기 스레드를 깨워 조건에 따라 진행하게 합니다.안전하고 신뢰가 가능합니다.
profile
🤔오늘의 복습은❓

0개의 댓글