[java] 자바 : notify () 대 notifyAll () 다시

만약 “의 차이에 대해 하나의 구글 notify()notifyAll()“다음 설명의 많은 (떨어져 javadoc의 단락을 떠나) 나타납니다. 그것은 모두 최대 깨울되는 대기하는 스레드의 수를 요약된다 : 하나 notify()와 모두를 notifyAll().

그러나 (이러한 방법의 차이점을 올바르게 이해하면) 모니터를 추가로 획득하기 위해 항상 하나의 스레드 만 선택됩니다. 첫 번째 경우에는 VM에 의해 선택된 것, 두 번째 경우에는 시스템 스레드 스케줄러에 의해 선택된 것. 두 가지 (일반적인 경우)에 대한 정확한 선택 절차는 프로그래머에게 알려지지 않았습니다.

무엇 유용 차이는 통지 ()의 notifyAll () 다음? 뭔가 빠졌습니까?



답변

그러나 (이러한 방법의 차이점을 올바르게 이해하면) 추가 모니터 수집을 위해 항상 하나의 스레드 만 선택됩니다.

맞지 않습니다. 호출 에서 차단 된 모든 스레드를 o.notifyAll()깨 웁니다 . 스레드는 하나씩 만 반환 수 있지만 각각 차례가됩니다.o.wait()o.wait()


간단히 말해 스레드가 알림을 기다리는 이유에 따라 다릅니다. 대기중인 스레드 중 하나에 무언가 발생했음을 알리고 싶거나 동시에 모든 스레드에 알리고 싶습니까?

경우에 따라 대기가 완료되면 모든 대기 스레드가 유용한 조치를 취할 수 있습니다. 예를 들어 특정 작업이 완료되기를 기다리는 일련의 스레드가 있습니다. 작업이 완료되면 모든 대기 스레드가 비즈니스를 계속할 수 있습니다. 이 경우 notifyAll () 을 사용 하여 대기중인 모든 스레드를 동시에 깨울 수 있습니다.

다른 경우, 예를 들어 상호 배타적 인 잠금의 경우 대기 스레드 중 하나만 알림을받은 후 유용한 작업을 수행 할 수 있습니다 (이 경우 잠금 획득). 이 경우 notify ()를 사용하는 것이 좋습니다 . 올바르게 구현되면 이 상황에서도 notifyAll () 을 사용할 있지만 어쨌든 아무것도 할 수없는 스레드를 불필요하게 깨울 것입니다.


대부분의 경우 조건을 기다리는 코드는 루프로 작성됩니다.

synchronized(o) {
    while (! IsConditionTrue()) {
        o.wait();
    }
    DoSomethingThatOnlyMakesSenseWhenConditionIsTrue_and_MaybeMakeConditionFalseAgain();
}

이렇게하면 o.notifyAll()호출이 둘 이상의 대기 스레드를 깨우고 make에서 돌아 오는 첫 번째 스레드 o.wait()가 조건을 false 상태로 유지하면 깨어 난 다른 스레드가 대기 상태로 돌아갑니다.


답변

분명히, notify대기 세트에서 하나의 스레드를 notifyAll깨우고 대기 세트의 모든 스레드를 깨 웁니다. 다음 논의는 의심을 없애야합니다. notifyAll대부분의 시간 동안 사용해야합니다. 어떤 것을 사용해야할지 확실하지 notifyAll않으면를 사용 하십시오. 다음 설명을 참조하십시오.

매우주의 깊게 읽고 이해하십시오. 질문이 있으시면 저에게 이메일을 보내 주시기 바랍니다.

생산자 / 소비자를보십시오 (가정은 두 가지 방법이있는 ProducerConsumer 클래스입니다). 그것은 (사용하기 때문에) 고장입니다 notify-그렇습니다-대부분의 경우에도 작동하지만 교착 상태를 일으킬 수 있습니다-우리는 이유를 볼 것입니다 :

public synchronized void put(Object o) {
    while (buf.size()==MAX_SIZE) {
        wait(); // called if the buffer is full (try/catch removed for brevity)
    }
    buf.add(o);
    notify(); // called in case there are any getters or putters waiting
}

public synchronized Object get() {
    // Y: this is where C2 tries to acquire the lock (i.e. at the beginning of the method)
    while (buf.size()==0) {
        wait(); // called if the buffer is empty (try/catch removed for brevity)
        // X: this is where C1 tries to re-acquire the lock (see below)
    }
    Object o = buf.remove(0);
    notify(); // called if there are any getters or putters waiting
    return o;
}

먼저

대기를 둘러싼 while 루프가 필요한 이유는 무엇입니까?

while이 상황이 발생 하면 루프 가 필요합니다 .

소비자 1 (C1)은 동기화 된 블록에 들어가고 버퍼가 비어 있으므로 C1은 ( wait통화 를 통해) 대기 세트에 놓입니다 . 소비자 2 (C2)는 동기화 된 메서드 (위의 Y 지점에서)에 들어 가려고하지만 생산자 P1은 버퍼에 개체를 넣은 다음을 호출합니다 notify. 대기중인 유일한 스레드는 C1이므로 깨우고 이제 X 지점 (위)에서 오브젝트 잠금을 다시 확보하려고 시도합니다.

이제 C1과 C2가 동기화 잠금을 획득하려고합니다. 그들 중 하나 (결정적이지 않게)가 선택되어 메소드에 들어가고, 다른 하나는 차단됩니다 (기다리는 것이 아니라 차단되어 메소드의 잠금을 얻으려고 시도합니다). C2가 먼저 잠금을 얻는다고 가정 해 봅시다. C1이 여전히 차단 중입니다 (X에서 잠금을 획득하려고 시도 함). C2는 메소드를 완료하고 잠금을 해제합니다. 이제 C1이 잠금을 획득합니다. whileC1이 루프 검사 (guard)를 수행하고 존재하지 않는 요소를 버퍼에서 제거 할 수 없기 때문에 루프 가있는 것이 무엇인지 추측 하십시오 (C2는 이미 그것을 얻었습니다!). 우리는이하지 않은 경우 while, 우리는을 얻을 것 IndexArrayOutOfBoundsExceptionC1 버퍼에서 첫 번째 요소를 제거하려고로!

지금,

자, 이제 우리는 notifyAll이 필요한 이유는 무엇입니까?

위의 생산자 / 소비자 예제에서 우리는 도망 갈 수있는 것처럼 보입니다 notify. 생산자와 소비자를위한 대기 루프 의 경비원 이 상호 배타적 임을 증명할 수 있기 때문에 이런 식으로 보입니다 . 즉, put메소드뿐만 아니라 메소드에서도 스레드를 대기 할 수없는 것처럼 보입니다 get.

buf.size() == 0 AND buf.size() == MAX_SIZE (MAX_SIZE가 0이 아니라고 가정)

그러나 이것은 충분하지 않습니다 notifyAll. 우리는 사용해야 합니다. 왜 그런지 보자 …

크기가 1 인 버퍼가 있다고 가정합니다 (예를 쉽게 따르기 위해). 다음 단계로 인해 교착 상태가 발생합니다. 통지로 스레드가 깨어 나면 언제든지 JVM에 의해 결정적으로 선택되지 않을 수 있습니다. 즉, 대기 스레드가 깨어날 수 있습니다. 또한 메소드에 진입 할 때 여러 스레드가 차단되는 경우 (즉, 잠금을 획득하려고 시도하는 경우) 획득 순서는 비 결정적 일 수 있습니다. 또한 스레드는 한 번에 하나의 메소드에만있을 수 있음을 기억하십시오. 동기화 된 메소드는 하나의 스레드 만 클래스에서 (동기화 된) 메소드를 실행 (즉, 잠금 보유) 할 수 있도록합니다. 다음과 같은 일련의 이벤트가 발생하면 교착 상태 결과가 발생합니다.

1 단계 :
-P1은 1 문자를 버퍼에 넣습니다.

2 단계 :
-P2 시도 put-대기 루프 확인-이미 문자-대기

3 단계 :
-P3 시도 put-대기 루프 확인-이미 문자-대기

4 단계 :
-C1이 1 개의 문자
를 가져 오려고 시도 함-C2가 1 개의 문자를 가져 오려고 시도 함- get메소드 에 진입 할 때 블록
-C3이 1 개의 문자를 얻으려고 시도 함- get메소드 에 진입 할 때 블록

STEP 5 :
– C1의 실행 get방법 – 숯, 호출 도착 notify, 이탈 방법
더 – notifyP2 깨어나는
-하지만, C2의 입장에서 P2 블록 있도록 (P2 잠금을 재 획득한다) 수 P2 전에있어서 진입 put방법
– C2 대기 루프를 검사하고 버퍼에 더 이상 문자가 없으므로 대기합니다
. C3은 C2 이후에 메소드에 들어가지만 P2 전에 대기 루프를 검사하고 버퍼에 더 이상 문자가 없으므로 대기합니다.

6 단계 :
-지금 : P3, C2 및 C3이 대기 중입니다!
-마지막으로 P2는 잠금을 획득하고 문자를 버퍼에 넣고 알림을 호출하고 메소드를 종료합니다.

7 단계 :
-P2의 알림이 P3을 깨 웁니다 (스레드가 깨어날 수 있음을 기억하십시오)
-P3는 대기 루프 조건을 확인합니다. 버퍼에 이미 문자가 있으므로 대기합니다.
-더 이상 알림을 호출 할 스레드가없고 3 개의 스레드가 영구적으로 일시 중단되었습니다!

해결책 : 교체 notifynotifyAll생산자 / 소비자 코드 (위)이다.


답변

유용한 차이점 :

  • 대기중인 모든 스레드가 상호 교환 가능하거나 깨우는 순서가 중요하지 않거나 대기 스레드가 하나 뿐인 경우 notify ()를 사용하십시오 . 일반적인 예는 대기열에서 작업을 실행하는 데 사용되는 스레드 풀입니다. 작업이 추가 될 때 스레드 중 하나에 깨우고 다음 작업을 실행하고 다시 절전 모드로 돌아가도록 통지됩니다.

  • 대기중인 스레드가 다른 목적을 가지고 동시에 실행될 수있는 다른 경우에는 notifyAll () 을 사용하십시오 . 예를 들어 공유 리소스에 대한 유지 관리 작업이 있습니다. 여기서 리소스에 액세스하기 전에 여러 스레드가 작업이 완료되기를 기다리고 있습니다.


답변

자원이 어떻게 생산되고 소비되는지에 달려 있다고 생각합니다. 한 번에 5 개의 작업 개체를 사용할 수 있고 5 개의 소비자 개체가있는 경우 notifyAll ()을 사용하여 모든 스레드를 깨우면 각 작업 개체가 1 개의 작업 개체를 처리 할 수 ​​있습니다.

사용 가능한 작업 개체가 하나 뿐인 경우 모든 소비자 개체를 깨워 해당 개체에 대해 경쟁하는 요점은 무엇입니까? 사용 가능한 작업을 확인하는 첫 번째 작업은 그것을 가져오고 다른 모든 스레드는 수행 할 작업이없는 것을 확인하고 찾습니다.

나는 여기서 좋은 설명을 찾았다 . 한마디로 :

notify () 메소드는 일반적으로 자원 을 취하는 임의의 수의 “소비자”또는 “작업자”가있는 자원 풀에 사용되지만 자원이 풀에 추가되면 대기중인 소비자 또는 작업자 중 하나만 처리 할 수 ​​있습니다. 그것으로. notifyAll () 메소드는 실제로 대부분의 다른 경우에 사용됩니다. 엄밀히 말하면, 웨이터에게 여러 웨이터가 진행할 수있는 상태를 알리는 것이 필요합니다. 그러나 이것은 종종 알기가 어렵다. 따라서 일반적으로 notify () 사용에 대한 특정 논리가 없으면 notifyAll ()을 사용해야합니다 . 왜냐하면 특정 객체에서 어떤 스레드가 대기하고 있는지, 왜 그 이유를 정확하게 알기가 어렵 기 때문입니다.


답변

동시성 유틸리티 당신은 또한 사이에 선택의 여지가 있음을 참고 signal()하고 signalAll()이러한 방법이있다라고한다. 따라서 질문은로도 유효합니다 java.util.concurrent.

더그 레아는 그의 흥미로운 점납니다 유명한 책을 하십시오 경우 notify()와는 Thread.interrupt()같은 시간에 일어나는는 실제로 길을 잃을 수도 통지합니다. notifyAll()오버 헤드 가격을 지불하더라도 (대부분의 스레드를 너무 많이 사용하더라도) 이러한 상황이 발생할 수 있고 극적인 의미를 갖는 것이 더 안전한 선택입니다.


답변

짧은 요약:

항상 선호 의 notifyAll ()을 통해 () 통지 는 많은 수의 스레드가 모두 같은 일을 할 대규모 병렬 응용 프로그램이없는 경우.

설명:

notify () […]는 단일 스레드를 깨 웁니다. 때문에 ()에 통보 하고, 많은 수의 스레드, 모든 일을 비슷한 집안일과 프로그램 – 당신이 깨어있는 스레드를 지정하는 것을 허용하지 않습니다, 그것은 단지 대규모 병렬 애플리케이션에서 유용하다. 이러한 응용 프로그램에서는 깨어 난 스레드를 신경 쓰지 않습니다.

출처 : https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html

위에서 설명한 상황에서 스레드가 동일한 작업을 수행하는 대규모 병렬 응용 프로그램 인 notify ()notifyAll () 과 비교하십시오 . 당신이 호출하는 경우 의 notifyAll ()를 하는 경우 의 notifyAll () , 하나의 스레드 만 실제로이 부여됩니다, 즉 스레드를 진행할 수 있기 때문에 (불필요하게 그들 중 많은 스레드의 거대한 숫자의 깨어 (즉, 스케줄링)을 유도 할 것이다 wait () , notify () 또는 notifyAll () 이 호출 된 오브젝트를 모니터하여 컴퓨팅 자원을 낭비합니다.

당신이 스레드의 거대한 숫자가 동시에 같은 일을하는 응용 프로그램이없는 경우에 따라서 선호 의 notifyAll ()가 이상 통지 () . 왜? 다른 사용자가이 포럼에서 이미 답변 했으므로 notify ()

이 객체의 모니터에서 대기중인 단일 스레드를 깨 웁니다. […] 선택은 임의적 이며 구현의 재량에 따라 발생합니다.

출처 : Java SE8 API ( https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#notify– )

소비자가 소비 할 준비가 된 (즉, wait () ing) 생산자가 준비 할 수있는 (즉, wait () ing) 품목의 생산 (소비 / 소비)이 비어 있는 생산자 소비자 애플리케이션이 있다고 가정합니다 . 이 경우, (가) 통지 까지 깨울되는 선택이기 때문에 단지 소비자와 결코 생산을 깨울 수있는 임의의 . 생산자와 소비자가 각각 생산하고 소비 할 준비가되어 있지만 생산자 소비자주기는 진전을 보이지 않습니다. 대신, 소비자는 깨어났습니다 (즉, wait () 상태를 떠나는 것 ). 비어 있기 때문에 항목을 대기열에서 꺼내지 않으며 다른 소비자가 notify () 를 진행합니다.

반대로 notifyAll () 은 생산자와 소비자 모두를 깨 웁니다. 스케줄 된 사람의 선택은 스케줄러에 따라 다릅니다. 물론 스케줄러의 구현에 따라 스케줄러는 컨슈머 만 스케줄 할 수 있습니다 (예 : 컨슈머 스레드에 우선 순위가 매우 높은 경우). 그러나 여기서는 스케줄러가 소비자 만 스케줄링하는 위험이 합리적으로 구현 된 스케줄러가 임의의 결정을 하지 않기 때문에 소비자 만 깨우는 JVM의 위험보다 낮다고 가정합니다 . 오히려 대부분의 스케줄러 구현은 기아를 방지하기 위해 최소한의 노력을 기울입니다.


답변

다음은 예입니다. 그것을 실행하십시오. 그런 다음 notifyAll () 중 하나를 notify ()로 변경하고 어떻게되는지 확인하십시오.

ProducerConsumerExample 클래스

public class ProducerConsumerExample {

    private static boolean Even = true;
    private static boolean Odd = false;

    public static void main(String[] args) {
        Dropbox dropbox = new Dropbox();
        (new Thread(new Consumer(Even, dropbox))).start();
        (new Thread(new Consumer(Odd, dropbox))).start();
        (new Thread(new Producer(dropbox))).start();
    }
}

Dropbox 클래스

public class Dropbox {

    private int number;
    private boolean empty = true;
    private boolean evenNumber = false;

    public synchronized int take(final boolean even) {
        while (empty || evenNumber != even) {
            try {
                System.out.format("%s is waiting ... %n", even ? "Even" : "Odd");
                wait();
            } catch (InterruptedException e) { }
        }
        System.out.format("%s took %d.%n", even ? "Even" : "Odd", number);
        empty = true;
        notifyAll();

        return number;
    }

    public synchronized void put(int number) {
        while (!empty) {
            try {
                System.out.println("Producer is waiting ...");
                wait();
            } catch (InterruptedException e) { }
        }
        this.number = number;
        evenNumber = number % 2 == 0;
        System.out.format("Producer put %d.%n", number);
        empty = false;
        notifyAll();
    }
}

소비자 클래스

import java.util.Random;

public class Consumer implements Runnable {

    private final Dropbox dropbox;
    private final boolean even;

    public Consumer(boolean even, Dropbox dropbox) {
        this.even = even;
        this.dropbox = dropbox;
    }

    public void run() {
        Random random = new Random();
        while (true) {
            dropbox.take(even);
            try {
                Thread.sleep(random.nextInt(100));
            } catch (InterruptedException e) { }
        }
    }
}

프로듀서 클래스

import java.util.Random;

public class Producer implements Runnable {

    private Dropbox dropbox;

    public Producer(Dropbox dropbox) {
        this.dropbox = dropbox;
    }

    public void run() {
        Random random = new Random();
        while (true) {
            int number = random.nextInt(10);
            try {
                Thread.sleep(random.nextInt(100));
                dropbox.put(number);
            } catch (InterruptedException e) { }
        }
    }
}