Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

forDevLife

[Java] notify() vs notifyAll() 본문

Java

[Java] notify() vs notifyAll()

JH_Lucid 2022. 1. 26. 16:14

참고 : https://stackoverflow.com/questions/37026/java-notify-vs-notifyall-all-over-again

 

Java: notify() vs. notifyAll() all over again

If one Googles for "difference between notify() and notifyAll()" then a lot of explanations will pop up (leaving apart the javadoc paragraphs). It all boils down to the number of waiting threads be...

stackoverflow.com

 

생각의 흐름

자바의 정석 Thread의 wait, notify 부분을 보다가 notify, notifyAll의 차이가 조금 헷갈렸다.

처음에 생각한 바로는 notify, notifyAll 둘 다 쓰레드가 동시에 사용하고자 하는 객체의 waiting pool에 있는 쓰레드 중 결과적으로 하나의 쓰레드가 임의로 lock을 획득하여 수행될 거라고 이해했다.

 

 

notify를 사용할 경우 발생하는 문제는?

우선 버퍼(사이즈 : 1)에 데이터를 담는 put 메서드, 데이터를 꺼내는 get 메서드를 가지는 객체가 있다고 가정하자. 메서드는 synchronized로 선언된다. 이를 사용하고자 하는 comsumer 쓰레드와 producer 쓰레드가 존재하며, consumer는 get을 호출할 거고 producer는 put을 호출할 것이다. 한 번에 하나의 쓰레드만 lock을 획득하여 실행할 수 있다.

 

각 메서드는 다음과 같은 while문을 가지고 있다.

public synchronized void put(Object o) {
    while (buf.size()==1) {
        wait();
    }
    buf.add(o);
    notify(); 
}

public synchronized Object get() {
    while (buf.size()==0) {
        wait();
    }
    Object o = buf.remove(0);
    notify(); 
    return o;
}
  • put : buf 사이즈가 1이라면, 쓰레드는 wait 상태로 들어가고 락을 넘긴다.
  • get : buf 사이즈가 0이라면, 쓰레드는 wait 상태로 들어가고 락을 넘긴다.

 

이 때 Producer 3개와(P1, P2, P3) Consumer 3개(C1, C2, C3)가 각각 해당 메서드를 호출하고 싶어한다.

 

 

Step 1 :

  • P1이 1개의 char을 버퍼로 넣었다.(P1 has Lock)
  • waiting : 없음 
  • finish : P1(step1에서 종료됨)

 

Step 2, 3 :

  • 그 다음 P2, P3가 각각 순서대로 버퍼에 put을 시도했으나, 꽉 차있기 때문에 wait 상태로 넘어갔다.
  • P1 has Lock -> P2 has Lock
  • waiting : P2, P3
  • finish : P1

 

Step 4 :

  • 이번에는 Consumer 3개가 동시에 get을 시도한다. 1개만 Lock을 획득하여 메서드를 실행하고, 나머지 2개는 block 될 것이다.
  • 맨 처음 C1이 Lock을 잡아 get을 성공했다.(C1 has Lock) 즉시 notify를 호출하여 waiting 상태의 쓰레드를 깨운다.
  • waiting 상태의 P2(랜덤)가 깨어났다. 하지만 도중에 C2가 먼저 Lock을 획득했다.(C2 has Lock)
  • 불행하게도 P2는 waiting 상태에서 벗어나긴 했지만, block 상태로 다시 Lock을 획득하고자 대기한다.
  • Lock을 가진 C2는 get을 수행한다. 하지만 버퍼의 크기가 0이므로, wait 상태로 넘어간다.
  • 다시 한번 불행하게도, C3가 P2보다 먼저 Lock을 잡아버렸다. 마찬가지로 get을 시도하며 버퍼의 크기가 0이므로 wait 상태로 넘어간다.
  • waiting : P3, C2, C3
  • finish : P1, C1(step 4에서 종료됨)

 

Step 5 :

  • 마침내 block된 P2가 Lock을 획득하고 버퍼에 char을 추가했다. notify를 호출하고, 메서드를 종료했다.
  • waiting : P3, C2, C3
  • finish : P1(step 5에서 종료됨), P2, C1

 

Step 6:

  • P2의 notify로 인해 임의(주의!)로 P3가 깨어났다. block 상태인 쓰레드가 없으므로 P3가 즉시 수행된다.
  • 하지만 버퍼가 꽉 찬 상태로 다시 P3는 wait 상태로 넘어간다.
  • waiting : C2, C3, P3(다시 추가 됨)
  • 현 시점에 blocking 된 쓰레드는 없다. 이 말은 즉슨 더 이상 notify를 호출해 줄 쓰레드는 없다는 의미와 같다.
    따라서 남은 3개의 쓰레드는 Permanently suspended 된다!

 

 

해결 : notifyAll을 사용한다.
  • notifyAll을 사용하게 되면, waiting 상태에 있는 모든 쓰레드가 다시 활성 상태가 된다. 비록 Blocking으로 인해 어떤 쓰레드가 먼저 메서드를 실행할지는 모르지만, 모든 쓰레드가 다시 한번 while 상태를 체크해서 자신의 동작 가능 여부를 확인할 수 있게 된다.
  • 앞서 Step 6 주의! 부분에서 P3 뿐 아니라 C2, C3도 wait 상태에서 벗어날 수 있는 기회가 주어진다.
    따라서 Step6 처럼 P3가 아무일도 못한 채 다시 wait 상태로 들어가더라도, 그 다음은 C2 또는 C3가 수행되어 버퍼를 소모하고 notifyAll()을 호출해 줄 수 있게 된다.

 

결론

Step 6에서 C2 또는 C3가 notify의 수혜를 받을 수는 있어도 그게 보장되지는 않는다. 결국 운이 나쁘게 되면 waiting 상태로 계속 기다리게 되는데 이를 기아(starvation) 현상 이라고 한다. 이를 해결하기 위해 notifyAll을 사용한다.

하지만 앞서 단계에서 문제가 된 점은 Consumer가 notify를 받았으면 했는데 Producer가 임의로 선택되었다는 점이다. notifyAll을 하게 되면 불필요하게 Producer도 Lock을 획득하기 위한 경쟁에 참여하게 된다. 이를 개선하기 위해 쓰레드를 구분해서 통지하는 것이 필요하다. Lock, Condition을 활용하여 선별적인 통지가 가능하다.

'Java' 카테고리의 다른 글

[Java] join과 Atomic 처리  (0) 2022.01.27
[Java] Call back(Listener)  (0) 2021.11.18
[Java] Recursive Type Bound  (0) 2021.11.14
[정규식] 문자열 계산기  (0) 2021.09.30
[java] import, static import 문  (0) 2021.06.09
Comments