티스토리 뷰

Object 객체의 wait(), notify(), notifyAll() 메서드란?

 sychronized를 사용할 때 락을 획득하지 못하여 무한 대기하는 경우에 Object 메서드 내 wait(), notify(), notifyAll()를 활용하여 해결할 수 있다.

  • wait(): 현재 스레드가 가진 락을 반납하고 대기(WAITING)한다. 현재 스레드가 sychronized 블록이나 메서드에서 락을 소유하고 있을 때만 호출할 수 있다. 호출 후 다른 스레드가 락을 획득할 수 있고, 대기 상태로 전환된 스레드는 다른 스레드가 notify(), notifyAll()을 호출할 때까지 대기 상태를 유지한다.
  • notify(): 대기 중인 스레드 중 하나를 깨운다. notify()는 sychronized 블록이나 메서드에서 호출되어야 한다. 깨운 스레드는 락이 존재할 경우 락을 획득하며, 대기 중인 스레드가 여러 개이면 그중 하나만 깨운다.
  • notifyAll(): 대기 중인 모든 스레드를 깨운다. 메서드 역시 sychronized 블록이나 메서드에서 호출되어야 한다.

wait(), notify() 예제

 아래 예제는 Queue 자료구조를 활용한 생산자(Queue에 데이터를 저장) 소비자(Queue에 데이터를 소비) 예제이며 wait(), notify() 메서드를 활용한 예제이다. 두가지 메서드를 사용 전과 후로 나뉘었다.

 

wait(), notify() 사용 전

BoundedQueue.java (공통 소스)

public interface BoundedQueue {

    void put(String data);

    String take();
}

 위의 인터페이스를 통해 핵심 구현체 로직을 2개(wait(), notify() 사용 전/후)로 구현하였다.

 

BoundedQueueV1.java

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) {
        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(), take() 메서드는 sychronized를 사용했다.

 

BoundedMain.java (공통 소스)

public class BoundedMain {

    public static void main(String[] args) {

        BoundedQueue queue = new BoundedQueueV1(2);
        //BoundedQueue queue = new BoundedQueueV2(2);

        producerFirst(queue); // 생산자 먼저 실행
        //consumerFirst(queue);
    }

    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 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 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);
        }
    }

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

 위는 실행 메서드이다. V1,V2는 사용 전과 후이며 생산자 먼저 실행할 때와 소비자 먼저 실행할 때로 나눌 수 있다.

 

생산자 먼저 실행 시 실행 결과

18:24:48.591 [     main] 생산자 시작
18:24:48.605 [producer1] [생산 시도] data1 -> []
18:24:48.606 [producer1] [생산 완료] data1 -> [data1]
18:24:48.705 [producer2] [생산 시도] data2 -> [data1]
18:24:48.706 [producer2] [생산 완료] data2 -> [data1, data2]
18:24:48.815 [producer3] [생산 시도] data3 -> [data1, data2]
18:24:48.816 [producer3] [put] 큐가 가득 참, 생산자 대기

18:24:48.926 [     main] 현재 상태 출력, 큐 데이터: [data1, data2]
18:24:48.927 [     main] producer1: TERMINATED
18:24:48.928 [     main] producer2: TERMINATED
18:24:48.928 [     main] producer3: TIMED_WAITING

18:24:48.929 [     main] 소비자 시작
18:24:48.933 [consumer1] [소비 시도]     ?  <- [data1, data2]
18:24:49.034 [consumer2] [소비 시도]     ?  <- [data1, data2]
18:24:49.135 [consumer3] [소비 시도]     ?  <- [data1, data2]

18:24:49.236 [     main] 현재 상태 출력, 큐 데이터: [data1, data2]
18:24:49.237 [     main] producer1: TERMINATED
18:24:49.238 [     main] producer2: TERMINATED
18:24:49.239 [     main] producer3: TIMED_WAITING
18:24:49.241 [     main] consumer1: BLOCKED
18:24:49.244 [     main] consumer2: BLOCKED
18:24:49.245 [     main] consumer3: BLOCKED
18:24:49.246 [     main] == [생산자 먼저 실행] 종료, BoundedQueueV2 ==
18:24:49.825 [producer3] [put] 큐가 가득 참, 생산자 대기
18:24:50.837 [producer3] [put] 큐가 가득 참, 생산자 대기
18:24:51.849 [producer3] [put] 큐가 가득 참, 생산자 대기
18:24:52.860 [producer3] [put] 큐가 가득 참, 생산자 대기
18:24:53.868 [producer3] [put] 큐가 가득 참, 생산자 대기
18:24:54.877 [producer3] [put] 큐가 가득 참, 생산자 대기
18:24:55.882 [producer3] [put] 큐가 가득 참, 생산자 대기
18:24:56.889 [producer3] [put] 큐가 가득 참, 생산자 대기
18:24:57.899 [producer3] [put] 큐가 가득 참, 생산자 대기

 위의 실행 결과를 보면 마지막 생산자가 무한 대기에 빠진 것을 볼 수 있고, 소비자는 소비 시도를 못하고 스레드 상태가 BLOCKED에 빠진 것을 볼 수 있다. 그 이유는 sychronized를 통해 알 수 있는데, 생산자 스레드 1,2는 정상적으로 생산 후 락을 반납하였는데 문제는 스레드 3이 락을 획득 후 무한대기에 빠진 것이다. 이렇게 되면 소비자 스레드 1,2,3은 락을 획득하지 못하게 된다. (※ sychronized의 락은 인스턴스에 하나 밖에 존재하지 않는다.)

 

소비자 먼저 실행 시 실행 결과

18:36:42.300 [     main] 소비자 시작
18:36:42.310 [consumer1] [소비 시도]     ?  <- []
18:36:42.311 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
18:36:42.426 [consumer2] [소비 시도]     ?  <- []
18:36:42.537 [consumer3] [소비 시도]     ?  <- []

18:36:42.638 [     main] 현재 상태 출력, 큐 데이터: []
18:36:42.648 [     main] consumer1: TIMED_WAITING
18:36:42.649 [     main] consumer2: BLOCKED
18:36:42.649 [     main] consumer3: BLOCKED

18:36:42.649 [     main] 생산자 시작
18:36:42.652 [producer1] [생산 시도] data1 -> []
18:36:42.753 [producer2] [생산 시도] data2 -> []
18:36:42.854 [producer3] [생산 시도] data3 -> []

18:36:42.956 [     main] 현재 상태 출력, 큐 데이터: []
18:36:42.957 [     main] consumer1: TIMED_WAITING
18:36:42.958 [     main] consumer2: BLOCKED
18:36:42.959 [     main] consumer3: BLOCKED
18:36:42.960 [     main] producer1: BLOCKED
18:36:42.960 [     main] producer2: BLOCKED
18:36:42.961 [     main] producer3: BLOCKED
18:36:42.962 [     main] == [소비자 먼저 실행] 종료, BoundedQueueV2 ==
18:36:43.318 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
18:36:44.331 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
18:36:45.346 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
18:36:46.356 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
18:36:47.368 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
18:36:48.382 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
18:36:49.390 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기

 위의 실행 결과를 보면 생산자 스레드를 실행한 것과 마찬가지로 소비자 스레드1이 락을 획득 후 무한대기에 빠졌기 때문에 나머지 소비자, 생산자 스레드가 락을 획득하지 못하고 BLOCKED 상태에 빠지게 된다. (※ sychronized의 락은 인스턴스에 하나 밖에 존재하지 않는다.)

 

wait(), notify() 사용 후

BoundedQueueV2.java

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] 큐가 가득 참, 생산자 대기");
            try {
                wait();
                log("[put] 락 반납 후 스레드 대기");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        queue.offer(data);
        log("[put] 생산자 데이터 저장, notify() 호출");
        notify();
    }

    @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();
        return data;
    }

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

 위의 코드를 보면 wait(), notify() 메서드가 있다. wait() 메서드를 실행하면 획득한 락을 반납 후 객체 내의 스레드 대기 집합에 대기 상태 빠지고 락을 반납하였음으로 BLOCKED 상태인 다른 스레드가 락을 획득 후 실행한다. 그리고 notify() 메서드를 실행하면 대기 집합에 있는 대기 중인 스레드 중 하나를 깨운다. (단 대기 집합에 있는 스레드를 깨운다고 바로 실행하는 것이 아니라 락을 획득해야만 실행한다.)

 

생산자 먼저 실행 시 실행 결과

18:46:41.379 [     main] 생산자 시작
18:46:41.405 [producer1] [생산 시도] data1 -> []
18:46:41.406 [producer1] [put] 생산자 데이터 저장, notify() 호출
18:46:41.407 [producer1] [생산 완료] data1 -> [data1]
18:46:41.497 [producer2] [생산 시도] data2 -> [data1]
18:46:41.497 [producer2] [put] 생산자 데이터 저장, notify() 호출
18:46:41.498 [producer2] [생산 완료] data2 -> [data1, data2]
18:46:41.606 [producer3] [생산 시도] data3 -> [data1, data2]
18:46:41.607 [producer3] [put] 큐가 가득 참, 생산자 대기

18:46:41.714 [     main] 현재 상태 출력, 큐 데이터: [data1, data2]
18:46:41.715 [     main] producer1: TERMINATED
18:46:41.715 [     main] producer2: TERMINATED
18:46:41.716 [     main] producer3: WAITING

18:46:41.717 [     main] 소비자 시작
18:46:41.720 [consumer1] [소비 시도]     ?  <- [data1, data2]
18:46:41.721 [consumer1] [take] 소비자 데이터 획득, notify() 호출
18:46:41.722 [producer3] [put] 락 반납 후 스레드 대기
18:46:41.722 [consumer1] [소비 완료] data1 <- [data2]
18:46:41.723 [producer3] [put] 생산자 데이터 저장, notify() 호출
18:46:41.724 [producer3] [생산 완료] data3 -> [data2, data3]
18:46:41.822 [consumer2] [소비 시도]     ?  <- [data2, data3]
18:46:41.822 [consumer2] [take] 소비자 데이터 획득, notify() 호출
18:46:41.823 [consumer2] [소비 완료] data2 <- [data3]
18:46:41.932 [consumer3] [소비 시도]     ?  <- [data3]
18:46:41.933 [consumer3] [take] 소비자 데이터 획득, notify() 호출
18:46:41.934 [consumer3] [소비 완료] data3 <- []

18:46:42.040 [     main] 현재 상태 출력, 큐 데이터: []
18:46:42.040 [     main] producer1: TERMINATED
18:46:42.041 [     main] producer2: TERMINATED
18:46:42.041 [     main] producer3: TERMINATED
18:46:42.042 [     main] consumer1: TERMINATED
18:46:42.042 [     main] consumer2: TERMINATED
18:46:42.042 [     main] consumer3: TERMINATED

 위의 실행 결과를 보면 .721 초에서 notify()를 호출 후 대기 중이었던 생산자 3이  "락 반납 후 스레드 대기"라는 콘솔에 남아 있다. 왜 소비를 했는데 이와 같은 콘솔이 남는 이유는 바로 생산자 3이 wait()를 호출할 때의 위치이다. 해당 위치는 아직 큐가 가득 차 있을 때의 위치이므로 로그가 찍히게 된다. 그리고 다시 while문을 실행할 때는 소비자가 데이터를 소비했기 때문에 다시 생산을 완료하게 된다.

 

소비자 먼저 실행 시 실행 결과

19:01:51.752 [     main] 소비자 시작
19:01:51.762 [consumer1] [소비 시도]     ?  <- []
19:01:51.763 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
19:01:51.870 [consumer2] [소비 시도]     ?  <- []
19:01:51.870 [consumer2] [take] 큐에 데이터가 없음, 소비자 대기
19:01:51.979 [consumer3] [소비 시도]     ?  <- []
19:01:51.979 [consumer3] [take] 큐에 데이터가 없음, 소비자 대기

19:01:52.088 [     main] 현재 상태 출력, 큐 데이터: []
19:01:52.103 [     main] consumer1: WAITING
19:01:52.104 [     main] consumer2: WAITING
19:01:52.104 [     main] consumer3: WAITING

19:01:52.105 [     main] 생산자 시작
19:01:52.108 [producer1] [생산 시도] data1 -> []
19:01:52.108 [producer1] [put] 생산자 데이터 저장, notify() 호출
19:01:52.109 [consumer1] [take] 락 반납 후 스레드 대기
19:01:52.109 [producer1] [생산 완료] data1 -> [data1]
19:01:52.109 [consumer1] [take] 소비자 데이터 획득, notify() 호출
19:01:52.110 [consumer2] [take] 락 반납 후 스레드 대기
19:01:52.110 [consumer1] [소비 완료] data1 <- []
19:01:52.110 [consumer2] [take] 큐에 데이터가 없음, 소비자 대기
19:01:52.212 [producer2] [생산 시도] data2 -> []
19:01:52.213 [producer2] [put] 생산자 데이터 저장, notify() 호출
19:01:52.213 [producer2] [생산 완료] data2 -> [data2]
19:01:52.213 [consumer3] [take] 락 반납 후 스레드 대기
19:01:52.215 [consumer3] [take] 소비자 데이터 획득, notify() 호출
19:01:52.215 [consumer2] [take] 락 반납 후 스레드 대기
19:01:52.215 [consumer3] [소비 완료] data2 <- []
19:01:52.216 [consumer2] [take] 큐에 데이터가 없음, 소비자 대기
19:01:52.322 [producer3] [생산 시도] data3 -> []
19:01:52.323 [producer3] [put] 생산자 데이터 저장, notify() 호출
19:01:52.323 [producer3] [생산 완료] data3 -> [data3]
19:01:52.323 [consumer2] [take] 락 반납 후 스레드 대기
19:01:52.324 [consumer2] [take] 소비자 데이터 획득, notify() 호출
19:01:52.325 [consumer2] [소비 완료] data3 <- []

19:01:52.430 [     main] 현재 상태 출력, 큐 데이터: []
19:01:52.430 [     main] consumer1: TERMINATED
19:01:52.431 [     main] consumer2: TERMINATED
19:01:52.431 [     main] consumer3: TERMINATED
19:01:52.432 [     main] producer1: TERMINATED
19:01:52.432 [     main] producer2: TERMINATED
19:01:52.433 [     main] producer3: TERMINATED

 위의 실행 결과를 보면 소비자는 정상적으로 대기 상태에 빠졌지만 생산자 시작 부분에 약간 이상한 점이 있다. 그것은 생산하고 소비를 하고 생산을 하고 소비를 하고 그래야 하는데 생산을 하면 소비를 하고 다시 소비를 하는 과정이 있다. 그 문제점은 notify()에 있는데 notity() 메서드는 대기 중인 스레드 중에 하나를 실행한다는 것이다. 그것 때문에 불필요한 스레드가 실행되는 문제가 발생한다.

 

정리

 wait(), notify() 메서드를 통해 sychronized 락의 무한대기의 문제점을 해결할 수 있다. 하지만 notify() 메서드는 대기 중인 스레드 중에 하나를 깨우기 때문에 원하지 않은 스레드가 실행될 수 있다.

 


본 포스팅은 “김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성/인프런”를 학습한 내용을 정리한 것

'Java > Java' 카테고리의 다른 글

<Java> yield() 메서드란?  (0) 2025.05.10
<Java> 버퍼 이란?  (0) 2025.03.04
<Java> ReentrantLock이란?  (0) 2024.10.26
<Java> sychronized이란?  (1) 2024.10.22
<Java> volatile이란?  (0) 2024.10.18
댓글
최근에 올라온 글
«   2026/03   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함
Total
Today
Yesterday