wait, notify, notifyAll

한눈에 보기

wait, notify, notifyAll은 Object에 정의된 스레드 협력 메서드로 고유 락과 대기 집합 위에서 동작한다. while 루프 검사가 필수인 이유와 notifyAll이 안전한 기본값인 이유를 설명한다.

최종 업데이트 2026.09.17

wait, notify, notifyAll은 java.lang.Object에 정의된 스레드 협력 메서드로, 어떤 조건이 충족될 때까지 스레드를 재우고 조건이 바뀌었을 때 깨우는 데 사용한다. wait()를 호출한 스레드는 잡고 있던 고유 락(intrinsic lock)을 놓고 그 객체의 대기 집합(wait set)에 들어가며, 다른 스레드가 같은 객체에서 notify() 또는 notifyAll()을 호출하면 깨어나 락을 다시 얻은 뒤 실행을 이어 간다. 세 메서드 모두 해당 객체의 락을 쥔 상태, 즉 synchronized 블록 안에서만 호출할 수 있다.

왜 Object의 메서드인가

자바에서 락은 특정 객체의 고유 락이고, 조건 대기 역시 그 락과 한 몸으로 움직여야 한다. wait는 락을 놓으며 잠들고 깨어날 때 같은 락을 다시 획득해야 하므로, 대기와 통지는 락의 주인인 객체에 속하는 것이 자연스럽다. 모든 객체가 고유 락을 가지기에 모든 객체의 부모인 Object에 이 메서드들이 정의된 것이다. 락 없이 호출하면 IllegalMonitorStateException이 발생하는데, 락을 놓으려면 먼저 쥐고 있어야 하고, 검사하는 조건 자체가 락으로 보호되는 공유 상태이기 때문이다.

동작 방식: 대기 집합과 락의 이동

스레드가 obj.wait()를 부르면 세 가지가 일어난다. 첫째, obj의 고유 락을 해제한다. 둘째, obj의 대기 집합에 들어가 WAITING 상태가 된다. 셋째, 깨어난 뒤에는 곧바로 실행되는 것이 아니라 락 경쟁에 다시 참여해 락을 획득해야 synchronized 블록 안으로 돌아온다. notify()는 대기 집합에서 임의의 스레드 하나를, notifyAll()은 전부를 깨운다. 깨어났다는 것은 락 경쟁 자격을 되찾았다는 뜻일 뿐 조건 충족을 보장하지 않는다는 점이 중요하다.

그래서 wait는 반드시 조건을 검사하는 while 루프 안에서 호출해야 한다. if로 한 번만 검사하면, 통지 없이 깨어나는 허위 각성(spurious wakeup)이나, 깨어나 락을 얻기 전에 다른 스레드가 조건을 먼저 소비해 버리는 상황에서 잘못된 상태로 진행하게 된다.

생산자 소비자 예제

public class BoundedBuffer {
    private final Queue<Integer> queue = new ArrayDeque<>();
    private final int capacity = 10;

    public synchronized void put(int item) throws InterruptedException {
        while (queue.size() == capacity) { // if가 아니라 while
            wait();                        // 가득 차면 락을 놓고 대기
        }
        queue.add(item);
        notifyAll();                       // 소비자들을 깨운다
    }

    public synchronized int take() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();                        // 비어 있으면 락을 놓고 대기
        }
        int item = queue.remove();
        notifyAll();                       // 생산자들을 깨운다
        return item;
    }
}

이 예제에서 notify() 대신 notifyAll()을 쓴 이유를 이해해야 한다. 하나의 대기 집합에 생산자와 소비자가 섞여 잠들어 있는데 notify()가 하필 같은 역할의 스레드를 깨우면, 깨어난 스레드는 조건이 여전히 거짓이라 다시 잠들고 통지는 소실된다. 최악의 경우 모두가 잠든 채 깨어나지 못하는 사실상의 교착이 된다. 서로 다른 조건을 기다리는 스레드가 한 객체에 섞일 수 있다면 notifyAll()이 안전한 기본값이다.

notify와 notifyAll 비교

구분notify()notifyAll()
깨우는 대상대기 집합의 임의 스레드 1개대기 집합의 모든 스레드
안전성통지 소실 위험 있음안전, 조건 안 맞으면 다시 잠듦
비용낮음깨어난 스레드들의 락 경쟁 비용
적합한 경우모든 대기자가 같은 조건을 기다릴 때조건이 여러 종류이거나 확신이 없을 때

현대적인 대안

wait와 notify는 저수준 도구라 실무에서는 더 높은 수준의 추상화를 먼저 고려한다. java.util.concurrent의 BlockingQueue를 쓰면 위의 생산자 소비자 버퍼를 직접 구현할 필요 없이 put과 take가 대기까지 처리해 준다. 세밀한 제어가 필요하면 ReentrantLock과 Condition 조합이 낫다. Condition은 하나의 락에 대기실을 여러 개 만들 수 있어, notFull과 notEmpty를 분리해 필요한 쪽만 정확히 깨울 수 있다. 그 밖에 CountDownLatch, Semaphore, CompletableFuture 같은 동기화 도구도 상황별 대안이다. 그럼에도 wait와 notify의 원리는 이 모든 도구의 바탕이므로 면접에서 여전히 중요하게 다뤄진다. 참고로 wait는 스레드를 재우는 점에서 Thread.sleep과 비슷해 보이지만, sleep은 락을 놓지 않고 시간만 기다리는 반면 wait는 락을 놓고 통지를 기다린다는 본질적 차이가 있다.

면접에서 이렇게 물어본다

  • wait와 notify가 Thread가 아니라 Object의 메서드인 이유를 설명해 보라.
  • wait를 synchronized 블록 밖에서 호출하면 어떤 예외가 왜 발생하는가.
  • wait를 if가 아니라 while 루프에서 호출해야 하는 이유를 허위 각성과 연결해 설명해 보라.
  • notify와 notifyAll의 차이, 그리고 notifyAll이 안전한 기본값인 이유를 설명해 보라.
  • wait와 Thread.sleep의 차이를 락 관점에서 설명해 보라.
Java 문서 전체 보기

이 개념, 내 코드에서는 어떻게 물어볼까요

데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.

공고 넣고 시작하기