자바의 wait()와 notify() 메서드

박시시·2022년 10월 5일
1

JAVA

목록 보기
10/13


(출처: https://www.baeldung.com/java-wait-notify)

위는 스레드의 라이프 사이클에 대한 간략한 다이어그램이다. 스레드는 생성된 후에 start() 메서드를 통해 실행된다. 정확히는 Runnable과 Running 상태를 왔다갔다 하며 실행되는데, 이는 JVM의 스레드 스케쥴러가 한정된 자원으로 인해 스레드를 교환해가며(컨텍스트 스위칭) 실행하기 때문이다.
멀티스레드 환경에서는 힙과 같은 스레드들이 공유하는 메모리 영역에서 동일한 자원에 접근해 해당 자원을 수정하는 작업을 할 수도 있다. 이러한 동시성 이슈를 제어하기 위해 여러 기법들이 사용된다. wait()와 notify(), notifyAll() 메서드 사용 역시 이러한 기법 중 하나이다.

wait()

자바에서 wait() 메서드는 스레드 동기화를 위한 Object의 인스턴스 메서드이다.
Object의 메서드이므로 어떤 객체에서도 호출이 가능하지만 오직 synchronized 블록 내에서만 호출이 가능하다. wait() 메서드의 기능이 객체에 대한 lock을 release하는 것이기 때문이며(좀 더 정확히는 제어권을 넘겨주는 것), 만약 wait()를 호출하는 스레드가 lock을 소유하고 있지 않다면 에러가 발생된다.
모니터락을 쥐고 있는 스레드가 (모니터락 객체에 대해) wait() 메서드를 호출하면(예를들어 lock.wait(1000)) 위에서 말했듯이 lock을 release하게 되고 다른 스레드가 해당 lock을 취득하게 된다.

sleep() 과의 차이점 - sleep() 메서드는 현재 스레드를 잠시 멈추게 할 뿐 lock을 release 하진 않는다. 즉 해당 잠든 스레드는 여전히 lock을 쥐고 있다.

wait()를 걸게 되면 lock을 소유하고 있던 스레드는 lock을 release하며 WAITING 또는 TIMED_WAITING 상태로 바뀌게 된다. 위의 그림에서 Non-Runnable 영역이 바로 그것이다.
스레드의 라이프 사이클에 대해 정리한 글은 여기에서 확인할 수 있다.

이렇게 WAITING 또는 TIMED_WAITING 상태에 들어가게 된 스레드는 notify() 혹은 notifyAll() 메서드를 호출함으로써 RUNNABLE 상태로 변경될 수 있다. 물론 TIMED_WAITING 상태의 스레드의 경우(wait(1000)과 같이 생성자를 이용하여 특정한 시간을 설정하여 대기) 특정한 시간이 지나면 자동으로 RUNNABLE 상태로 변경된다.

notify(), notifyAll()

모니터 락 객체에 wait를 건 모든 스레드들에 대해 notify() 메서드를 사용하면 그 스레드들 중 하나를 임의로 깨우게 된다. 어느 스레드를 깨울지는 정해져있지 않으며 물론 구현에 따라 달라질 수도 있다.
notifyAll() 메서드는 이름에서 알 수 있듯이, wait를 건 모든 스레드들을 한 번에 깨우게 된다.

Consumer-Producer 예제

public class Data {

  public void produce() throws InterruptedException {
    synchronized(this) {
      System.out.println("producer thread running");
      
      // releases the lock on shared resource
      wait();

      System.out.println("Resumed");
    }
  }

  public void consume() throws InterruptedException {
    // producer 스레드가 먼저 실행되게끔 1초 슬립.
    Thread.sleep(1000);

    Scanner s = new Scanner(System.in);

    synchronized(this) {
      System.out.println("Waiting for return key.");
      s.nextLine();
      System.out.println("Return key pressed");

      notify();

      Thread.sleep(2000);
    }
  }
}
public class Producer implements Runnable {
  private Data data;

  // standard constructors

  public void run() {
    try {

      data.produce();

    } catch(InterruptedException e) {
      Thread.currentThread().interrupt(); 
      Log.error("Thread interrupted", e); 
    }
  }
}
public class Consumer implements Runnable {
  private Data data;

  // standard constructors

  public void run() {
    try {
      
      data.consume();

    } catch(InterruptedException e) {
      Thread.currentThread().interrupt(); 
      Log.error("Thread interrupted", e); 
    }
  }
}
public static void main(String[] args) {
  Data data = new Data();
  Thread producer = new Thread(new Producer(data));
  Thread consumer = new Thread(new Consumer(data));
  
  producer.start();
  consumer.start();

  // consumer 전에 producer가 종료된다
  producer.join();
  consumer.join();
}

[결과]

producer thread running
Waiting for return key.
Return key pressed
Resumed

wait()와 notify()에 집중하여 설명해 보자면,


(출처: https://www.geeksforgeeks.org/inter-thread-communication-java/)

  1. producer 스레드가 start 된 후 data.produce(); 메서드를 실행시킨다. 해당 메서드가 실행되며 producer 스레드는 data 객체에 대한 lock을 획득한다(위 그림 2번).
  2. 해당 메서드에서 "producer thread running" 를 출력한 후 wait() 메서드를 호출하며 소유하고 있던 data에 대한 lock을 release하게 된다(위 그림 3번). 위 그림 3번을 보면 Wait Set 영역으로 들어가는 것을 볼 수 있다. 물론 이 Wait Set 실제 존재하는 영역이 아닌 개념적인 영역이다. 여튼 해당 스레드의 state는 Waiting이 되며 위의 그림에 써있듯이 notified 되길 기다리는 상태이다.
  3. producer 스레드가 wait() 메서드를 호출하기 전, consumer 스레드는 data 객체에의 접근이 BLOCKED 된 상태였다. producer 스레드에서 해당 객체에 대해 wait() 메서드를 호출했으므로 이제 consumer 스레드는 해당 객체에 접근이 가능하며 data.consume(); 메서드를 실행하게 된다.
  4. 먼저 "Waiting for return key." 메시지를 출력하고, 키를 입력받는다.
  5. 키를 입력받은 후엔 "Return key pressed" 메시지를 출력하게 된다.
  6. consumer 스레드에서 notify() 메서드가 호출된다.
  7. notify() 메서드는 해당 객체의 lock을 waiting 하고 있는 스레드들 중 하나를 임의로 Runnable 상태로 돌아오게 한다(위 그림 4번). 하지만 notify() 메서드가 lock을 release 하는 것은 아니다. 즉 notify() 한다고 waiting하던 스레드 하나가 바로 lock을 획득하는 것이 아니다. 위 코드에서처럼 consumer 스레드에서 Thread.sleep(2000);를 실행한 후 해당 메서드가 종료되면 그 때 lock이 release 된다.
  8. 위 코드 상에선 waiting하던 스레드가 producer 밖에 없으므로 producer 스레드가 경쟁없이 바로 data 객체에 대한 lock을 획득하게 된다(위 그림 5번).
  9. "Resumed" 메시지를 출력한 후 producer 스레드가 종료된다(위 그림 6번).

위의 순서대로 작동된다.

참조

https://www.baeldung.com/java-wait-notify
https://www.baeldung.com/java-wait-and-sleep
https://javaplant.tistory.com/29
https://www.geeksforgeeks.org/inter-thread-communication-java/

0개의 댓글