Wait과 Notify, Spurious Wakeup

 

 

Java 기준으로 설명

https://www.baeldung.com/java-wait-notify

Wait과 Notify

자바에서 모든 객체는 모니터를 갖는다.

그리고 스레드가 synchronized에 들어설 때, 해당 스레드가 객체의 **모니터**를 소유한다.

  • 주어진 객체에 대해 synchrnoized instance 메서드를 실행했을 때
  • 주어진 객체에서 synchrnoized block이 실행될 때
  • synchronized static 메서드를 실행할 때

<aside> ✅

synchrnoized (feat. claude)

https://www.baeldung.com/cs/monitor

synchronized의 작동 방식

  1. 모든 Java 객체는 고유한 모니터(monitor)를 가지고 있습니다. 이 모니터는 잠금 장치처럼 작동합니다.
  2. 스레드가 synchronized 블록이나 메서드에 진입하려고 할 때:
    • 해당 객체의 모니터가 사용 가능하다면, 스레드는 모니터를 획득하고 코드를 실행합니다.
    • 모니터가 다른 스레드에 의해 사용 중이라면, 현재 스레드는 모니터가 해제될 때까지 대기합니다.
  3. synchronized 블록이나 메서드가 종료되면, 스레드는 자동으로 모니터를 해제합니다. </aside>

wait()

wait() 메서드는 다른 스레드가 이 객체에 대해 notify()를 호출하거나 notifyAll()을 호출할 때까지 현재 스레드가 대기하도록 만든다.

  • wait(): 무한정 대기
  • wait(long timeout): 타임아웃 지정
  • wait(long timeout, int nanos): 타임아웃 지정(나노초 까지)

notify()

이 객체의 모니터에 대한 액세스를 기다리고 있는 쓰레드를 깨운다.

  • notify(): 이 객체의 모니터를 대기중인 스레드 중 임의로 하나를 깨운다.
    • 정확히 어떤 스레드를 깨울지는 비결정적이며 구현에 따라 달라진다.
  • notifyAll(): 대기중인 모든 스레드를 깨운다

Sender-Receiver 동기화 문제

  • 발신자는 수신자에게 데이터 패킷을 보내야 한다.
  • 수신자는 발신자가 데이터 패킷 전송을 완료할 때까지 데이터 패킷을 처리할 수 없다.
  • 마찬가지로 발신자는 수신자가 이미 이전 패킷을 처리하지 않는 한 다른 패킷을 보내려고 시도해서는 안 된다.
public class Data {
    private String packet;
    
    // True이면 receiver는 기다려야한다.
    // False이면 sender는 기다려야한다.
    private boolean transfer = true;
 
    public **synchronized** String receive() {
        while (transfer) {
            try {
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); 
                System.err.println("Thread Interrupted");
            }
        }
        transfer = true;
        
        String returnPacket = packet;
        notifyAll();
        return returnPacket;
    }
 
    public **synchronized** void send(String packet) {
        while (!transfer) {
            try { 
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); 
                System.err.println("Thread Interrupted");
            }
        }
        transfer = false;
        
        this.packet = packet;
        notifyAll();
    }
}

 

Synchronized 블록안에 wait()이 무슨의미지? 어차피 모니터를 갖는다며!

Q.

위 코드에서 synchronized 메서드를 실행하려는 두 메서드가 있는 상황.
두 메서드 중 하나만 모니터를 획득하고 메서드를 정상적으로 실행, 
모니터를 획득하지 못한 스레드는 synchronized 블록 앞에서 모니터의 해제를 대기하고 있을텐데
wait이 synchronized 메서드 안에 있을 이유는 뭐지

A. 이것은 매우 좋은 질문입니다. wait()이 synchronized 블록 안에 있어야 하는 이유를 설명드리겠습니다 (by. claude)

  1. 경쟁 조건(Race Condition) 방지
    • wait()을 호출하는 순간 스레드는 객체의 모니터 락을 해제하고 대기 상태로 들어갑니다
    • 만약 wait()이 synchronized 블록 밖에 있다면, 조건 검사(while (transfer) 또는 while (!transfer))와 wait() 호출 사이에 다른 스레드가 끼어들 수 있습니다
    • 이는 심각한 동기화 문제를 일으킬 수 있습니다
  2. 필수적인 동기화 매커니즘
    1. 아래 두 작업이 하나의 원자적 작업(atomic operation)으로 실행되어야 합니다
    2. synchronized 없이는 조건 검사와 대기 사이에 다른 스레드가 상태를 변경할 수 있습니다
// 예를 들어, receive() 메서드에서:
while (transfer) {  // 조건 검사
    wait();         // 대기
}

실제 동작 순서를 보면:

  1. 스레드가 synchronized 메서드에 진입하여 모니터 락을 획득
  2. 조건을 검사하고 wait()이 필요한 경우
  3. wait() 호출 시 모니터 락을 자동으로 해제
  4. 다른 스레드가 이제 동기화 블록에 진입 가능
  5. notifyAll() 호출 시 대기중인 스레드가 깨어나고 다시 락을 획득하기 위해 경쟁

따라서 wait()은 synchronized 블록 안에 있어야만 스레드 간의 안전한 통신과 동기화를 보장할 수 있습니다.

 

항상, 스레드의 동작 조건을 확인하는 빠른 검사를 작성하라.

말이 좀 어려운데..

wait()을 while()문으로 감싸는 것을 말한다.

    public synchronized String receive() {
        **while (transfer) {**
            try {
                **wait();**
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); 
                System.err.println("Thread Interrupted");
            }
        }
        transfer = true;
    }

⇒ notify를 받지않고 스레드가 깨어나는 상황(Spurious Wakeup)이 있을 수 있기 때문

Spurious Wakeup

한국말로도 스퓨리어스 웨이크업..

스레드가 wait() 상태에서 명시적인 notify() 호출 없이도 깨어나는 현상

운영체제의 최적화나 하드웨어적인 이유로 발생할 수 있다. → 어쩔수없다. 이걸 처리하는 것은 개발자의 몫

class Buffer {
    private int count = 0;
    
    public synchronized void consume() {
        if (count == 0) {  // 잘못된 구현
            try {
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        count--;
    }
}

위 코드는 언뜻 보기에 문제가 없어 보인다.

다만, 이러한 코드는 Spurious Wakeup에 취약한데.

Spurious Wakeup으로 스레드가 임의로 깨어나서 조건이 충족되지 않은 상태에서 count— 연산을 수행할 수 있다.

class Buffer {
    private int count = 0;
    
    public synchronized void consume() {
        while (count == 0) {  // 잘못된 구현
            try {
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        count--;
    }
}

위와 같이 작성하면 Spurious Wakeup이 일어나 조건이 만족되지 않은 채로 깨어난 스레드들은 while 루프를 통해 다시 wait() 하게된다.

댓글