(출처: https://www.baeldung.com/java-wait-notify)
위는 스레드의 라이프 사이클에 대한 간략한 다이어그램이다. 스레드는 생성된 후에 start() 메서드를 통해 실행된다. 정확히는 Runnable과 Running 상태를 왔다갔다 하며 실행되는데, 이는 JVM의 스레드 스케쥴러가 한정된 자원으로 인해 스레드를 교환해가며(컨텍스트 스위칭) 실행하기 때문이다.
멀티스레드 환경에서는 힙과 같은 스레드들이 공유하는 메모리 영역에서 동일한 자원에 접근해 해당 자원을 수정하는 작업을 할 수도 있다. 이러한 동시성 이슈를 제어하기 위해 여러 기법들이 사용된다. wait()와 notify(), notifyAll() 메서드 사용 역시 이러한 기법 중 하나이다.
자바에서 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 상태로 변경된다.
모니터 락 객체에 wait를 건 모든 스레드들에 대해 notify() 메서드를 사용하면 그 스레드들 중 하나를 임의로 깨우게 된다. 어느 스레드를 깨울지는 정해져있지 않으며 물론 구현에 따라 달라질 수도 있다.
notifyAll() 메서드는 이름에서 알 수 있듯이, wait를 건 모든 스레드들을 한 번에 깨우게 된다.
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/)
data.produce();
메서드를 실행시킨다. 해당 메서드가 실행되며 producer 스레드는 data 객체에 대한 lock을 획득한다(위 그림 2번).data.consume();
메서드를 실행하게 된다.Thread.sleep(2000);
를 실행한 후 해당 메서드가 종료되면 그 때 lock이 release 된다.위의 순서대로 작동된다.
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/