wait(), notify()

Object.wait()

  • 현재 스레드가 가진 락을 반납하고 대기( WAITING )한다.
  • 현재 스레드를 대기( WAITING ) 상태로 전환한다. 이 메서드는 현재 스레드가 synchronized 블록이나 메서드에서 락을 소유하고 있을 때만 호출할 수 있다. 호출한 스레드는 락을 반납하고, 다른 스레드가 해당 락을 획득할 수 있도록 한다. 이렇게 대기 상태로 전환된 스레드는 다른 스레드가 notify() 또는 notifyAll() 을 호출할 때까지 대기 상태를 유지한다.

Object.notify()

  • 대기 중인 스레드 중 하나를 깨운다.
  • 이 메서드는 synchronized 블록이나 메서드에서 호출되어야 한다. 깨운 스레드는 락을 다시 획득할 기회를 얻게 된다. 만약 대기 중인 스레드가 여러 개라면, 그 중 하나만이 깨워지게 된다.

Object.notifyAll()

  • 대기 중인 모든 스레드를 깨운다.
  • 이 메서드 역시 synchronized 블록이나 메서드에서 호출되어야 하며, 모든 대기 중인 스레드가 락을 획득할 수 있는 기회를 얻게 된다. 이 방법은 모든 스레드를 깨워야 할 필요가 있는 경우에 유용하다.

생산자 소비자 문제 코드

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

put(data) - wait(), notify()**

  • synchronized 를 통해 임계 영역을 설정한다. 생산자 스레드는 락 획득을 시도한다.
  • 락을 획득한 생산자 스레드는 반복문을 사용해서 큐에 빈 공간이 생기는지 주기적으로 체크한다. 만약 빈 공간이\없다면 Object.wait() 을 사용해서 대기한다. 참고로 대기할 때 락을 반납하고 대기한다. 그리고 대기 상태에서 깨어나면, 다시 반복문에서 큐의 빈 공간을 체크한다.

take() - wait(), notify()**

  • synchronized 를 통해 임계 영역을 설정한다. 소비자 스레드는 락 획득을 시도한다.
  • 락을 획득한 소비자 스레드는 반복문을 사용해서 큐에 데이터가 있는지 주기적으로 체크한다. 만약 데이터가 없다면 Object.wait() 을 사용해서 대기한다. 참고로 대기할 때 락을 반납하고 대기한다. 그리고 대기 상태에서 깨어나면, 다시 반복문에서 큐에 데이터가 있는지 체크한다.
  • 대기하는 경우 RUNNABLE WAITING 상태가 된다.
  • 소비자가 데이터를 획득하고 나면 notify() 를 통해 큐에 저장할 여유 공간이 생겼다고, 대기하는 스레드에게 알려주어야 한다. 예를 들어서 큐에 데이터가 가득 차서 대기하는 생산자 스레드가 있다고 가정하자. 이때 notify() 를 호출하면 생산자 스레드는 깨어나서 데이터를 큐에 저장할 수 있다.

wait() 로 대기 상태에 빠진 스레드는 notify() 를 사용해야 깨울 수 있다. 생산자는 생산을 완료하면 notify()로 대기하는 스레드를 깨워서 생산된 데이터를 가져가게 하고, 소비자는 소비를 완료하면 notify() 로 대기하는 스레드를 깨워서 데이터를 생산하라고 하면 된다. 여기서 중요한 핵심은 wait() 를 호출해서 대기 상태에 빠질 때 락을 반납하고 대기 상태에 빠진다는 것이다. 대기 상태에 빠지면 어차피 아무일도 하지 않으므로 락도 필요하지 않다.

모든 객체는 락(모니터 락)과 대기 집합을 가지고 있다

![](https://velog.velcdn.com/images/guns95/post/45913b79-0f0d-46ed-805f-a1e7fa1dead3/image.png

스레드 대기 집합(wait set)

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

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

큐가 가득찼을 때의 wait 동작

  • p3 가 데이터를 생산하려고 하는데, 큐가 가득 찼다. wait() 를 호출한다.
  • wait()` 를 호출하면
    • 락을 반납한다.
    • 스레드의 상태가 RUNNABLE WAITING 로 변경된다.
    • 스레드 대기 집합에서 관리된다.
  • 스레드 대기 집합에서 관리되는 스레드는 이후에 다른 스레드가 notify() 를 통해 스레드 대기 집합에 신호를주면 깨어날 수 있다.

소비자 쓰레드 실행으로 notify() 동작

소비자 쓰레드가 락을 획득한 후,

  • 소비자 스레드가 데이터를 획득했기 때문에 큐에 데이터를 보관할 빈자리가 생겼다.
  • 소비자 스레드는 notify() 를 호출해서 스레드 대기 집합에 이 사실을 알려준다.

스레드대기열에 notify() 전달 후

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

notify() 로 깨어난 생성자 쓰레드

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

Object - wait, notify - 한계

지금까지 살펴본 Object.wait() , Object.notify() 방식은 스레드 대기 집합 하나에 생산자, 소비자 스레드를모두 관리한다. 그리고 notify() 를 호출할 때 임의의 스레드가 선택된다. 따라서 앞서 살펴본 것 처럼 큐에 데이터가없는 상황에 소비자가 같은 소비자를 깨우는 비효율이 발생할 수 있다. 또는 큐에 데이터가 가득 차있는데 생산자가 같은 생산자를 깨우는 비효율도 발생할 수 있다. -> 스레드 기아 발생할 수 있음

이런 문제점을 해결 할 수있는 방법을 다음 블로그에 정리해보겠다.

profile
Live the moment for the moment.

0개의 댓글