Java에서 wait () 및 notify ()를 사용하는 간단한 시나리오


181

완벽하게 간단한 시나리오, 예를 들어 Queue와 함께 사용하는 방법을 제안하는 자습서를 얻을 수 있습니까?

답변:


269

wait()notify()방법은 특정 조건이 만족 될 때까지 차단하는 실을 수 있도록하는 메커니즘을 제공하도록 설계된다. 이를 위해 고정 크기의 백업 저장소 요소가있는 블로킹 큐 구현을 작성하려고한다고 가정합니다.

가장 먼저해야 할 일은 메소드가 기다리는 조건을 식별하는 것입니다. 이 경우 put()상점에 여유 공간이 확보 take()될 때까지 메소드를 차단하고 리턴 할 요소가있을 때까지 메소드를 차단 하려고합니다 .

public class BlockingQueue<T> {

    private Queue<T> queue = new LinkedList<T>();
    private int capacity;

    public BlockingQueue(int capacity) {
        this.capacity = capacity;
    }

    public synchronized void put(T element) throws InterruptedException {
        while(queue.size() == capacity) {
            wait();
        }

        queue.add(element);
        notify(); // notifyAll() for multiple producer/consumer threads
    }

    public synchronized T take() throws InterruptedException {
        while(queue.isEmpty()) {
            wait();
        }

        T item = queue.remove();
        notify(); // notifyAll() for multiple producer/consumer threads
        return item;
    }
}

대기 및 알림 메커니즘을 사용해야하는 방법에 대해 몇 가지 참고할 사항이 있습니다.

첫째, 당신은 보장하기 위해 필요로하는 모든 호출에 wait()notify()코드의 동기화 된 지역 내합니다 (함께있는 wait()notify()같은 객체에 동기화되는 호출). 표준 스레드 안전 문제 이외의 이유는 누락 된 신호로 인한 것입니다.

예를 들어 put()대기열이 가득 차면 스레드가 호출 되어 조건을 확인하고 대기열이 가득 찼음을 확인하지만 다른 스레드가 예약되어 있기 전에 차단할 수 있습니다. 그런 다음이 두 번째 스레드 take()는 큐의 요소이며 대기 스레드에 큐가 더 이상 채워지지 않았 음을 알립니다. 그러나 첫 번째 스레드는 이미 조건을 확인했기 때문에 wait()진행이 가능하더라도 일정이 변경 되면 간단히 호출 됩니다.

공유 객체를 동기화 take()하면 첫 번째 스레드가 실제로 차단 될 때까지 두 번째 스레드의 호출이 진행될 수 없으므로이 문제가 발생하지 않도록 할 수 있습니다.

둘째, 가짜 웨이크 업으로 알려진 문제로 인해 if 문이 아닌 while 루프에서 점검중인 조건을 넣어야합니다. 대기 스레드를 notify()호출 하지 않고 재 활성화 할 수있는 경우가 있습니다 . 이 검사를 while 루프에 넣으면 가짜 웨이크 업이 발생하면 조건이 다시 검사되고 스레드가 wait()다시 호출 됩니다.


다른 답변 중 일부에서 언급했듯이 Java 1.5 java.util.concurrent는 대기 / 알림 메커니즘에 대해 더 높은 수준의 추상화를 제공하도록 설계된 새로운 동시성 라이브러리 ( 패키지)를 도입했습니다 . 이러한 새로운 기능을 사용하면 다음과 같이 원래 예제를 다시 작성할 수 있습니다.

public class BlockingQueue<T> {

    private Queue<T> queue = new LinkedList<T>();
    private int capacity;
    private Lock lock = new ReentrantLock();
    private Condition notFull = lock.newCondition();
    private Condition notEmpty = lock.newCondition();

    public BlockingQueue(int capacity) {
        this.capacity = capacity;
    }

    public void put(T element) throws InterruptedException {
        lock.lock();
        try {
            while(queue.size() == capacity) {
                notFull.await();
            }

            queue.add(element);
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while(queue.isEmpty()) {
                notEmpty.await();
            }

            T item = queue.remove();
            notFull.signal();
            return item;
        } finally {
            lock.unlock();
        }
    }
}

물론 실제로 차단 대기열이 필요한 경우 BlockingQueue 인터페이스 의 구현을 사용해야합니다 .

또한 이와 같은 것들 에 대해서는 동시성 관련 문제 및 솔루션에 대해 알아야 할 모든 것을 다루기 때문에 Java Concurrency in Practice를 강력히 권장 합니다.


7
@greuze, notify하나의 스레드 만 깨 웁니다. 두 개의 소비자 스레드가 요소를 제거하기 위해 경쟁하는 경우 하나의 알림이 다른 소비자 스레드를 깨울 수 있습니다.이 알림은 그것에 대해 아무 것도 할 수 없으며 다시 생산자 대신 새 요소를 삽입하기를 원했던 잠자기 상태로 돌아갑니다. 생산자 스레드가 깨어나지 않고 아무 것도 삽입되지 않으며 이제 세 스레드가 모두 무한정 잠자 게됩니다. 가짜 웨이크 업이 문제의 원인이라고 잘못 말한대로 이전 의견을 삭제했습니다. (
아니요

1
@finnw 내가 알 수있는 한, notifyAll ()을 사용하여 발견 한 문제를 해결할 수 있습니다. 내가 맞아?
클린트 이스트우드

1
@Jared가 제공 한 예는 꽤 좋지만 심각한 하락을 보입니다. 코드에서 모든 메소드는 동기화 된 것으로 표시되었지만 같은 시간에 두 개의 동기화 된 메소드를 실행할 수 없습니다. 그러면 그림에 두 번째 스레드가 어떻게 생깁니 까?
Shivam Aggarwal

10
@ Brut3Forc3 당신은 wait ()의 javadoc을 읽어야합니다 : 그것은 말합니다 : 쓰레드는이 모니터의 소유권을 해제 합니다. 따라서 wait ()가 호출 되 자마자 모니터가 해제되고 다른 스레드가 큐의 다른 동기화 된 메소드를 실행할 수 있습니다.
JB 니 제트

1
@JBNizet. "예를 들어, 큐가 가득 찼을 때 스레드가 put ()을 호출 할 수 있고, 상태를 점검하고, 큐가 가득 찼음을 확인하지만 다른 스레드가 예약되어있는 것을 차단할 수 있습니다." 대기가 아직 호출되지 않은 경우 두 번째 스레드가 예약됩니다.
Shivam Aggarwal

148

큐 예제는 아니지만 매우 간단합니다 :)

class MyHouse {
    private boolean pizzaArrived = false;

    public void eatPizza(){
        synchronized(this){
            while(!pizzaArrived){
                wait();
            }
        }
        System.out.println("yumyum..");
    }

    public void pizzaGuy(){
        synchronized(this){
             this.pizzaArrived = true;
             notifyAll();
        }
    }
}

몇 가지 중요한 사항 :
1) 절대로하지 마십시오

 if(!pizzaArrived){
     wait();
 }

항상 while (조건)을 사용하십시오.

  • a) 스레드는 아무에게도 알리지 않고 대기 상태에서 산발적으로 깨어날 수 있습니다. (피자 남자가 차임을 울리지 않더라도 누군가 피자를 먹어보기로 결정했을 것입니다.)
  • b) 동기화 잠금을 획득 한 후 상태를 다시 확인해야합니다. 피자가 영원히 지속되지 않는다고 가정 해 봅시다. 피자에 줄을 서서 깨우지 만 모든 사람에게 충분하지 않습니다. 확인하지 않으면 종이를 먹을 수 있습니다! :) (아마 좋은 예가 될 것 while(!pizzaExists){ wait(); }입니다.

2) 대기 / 없음을 호출하기 전에 잠금 (동기화)을 유지해야합니다. 스레드는 깨어나 기 전에 잠금을 획득해야합니다.

3) 동기화 된 블록 내에서 잠금을 얻지 말고 외계인 메서드 (어떻게하는지 잘 모르는 방법)를 호출하지 않도록 노력하십시오. 필요한 경우 교착 상태를 피하기위한 조치를 취하십시오.

4) notify ()에주의하십시오. 무엇을하고 있는지 알 때까지 notifyAll ()을 고수하십시오.

5) 마지막으로 Java Concurrency를 실제로 읽으십시오 !


1
"if (! pizzaArrived) {wait ();}"를 사용하지 않는 이유에 대해 자세히 설명해 주시겠습니까?
모두

2
@Everyone : 설명이 추가되었습니다. HTH.
Enno Shioji

1
pizzaArrived깃발을 사용합니까? 호출하지 않고 플래그를 변경 notify하면 아무런 영향을 미치지 않습니다. 또한 예제가 작동 wait하고 notify호출합니다.
Pablo Fernandez

2
이해하지 못합니다-스레드 1은 eatPizza () 메소드를 실행하고 최상위 동기화 블록을 입력하고 MyHouse 클래스에서 동기화합니다. 피자가 아직 도착하지 않았으므로 기다립니다. 이제 스레드 2는 pizzaGuy () 메소드를 호출하여 피자를 전달하려고 시도합니다. 스레드 1이 이미 잠금을 소유하고 있고 그것을 포기하지 않기 때문에 (영원히 기다리고 있습니다). 실제로 결과는 교착 상태입니다-스레드 1은 스레드 2가 notifyAll () 메소드를 실행하기를 기다리는 반면 스레드 2는 스레드 1이 MyHouse 클래스의 잠금을 포기하기를 기다리는 중입니다 ... 여기?
flamming_python

1
변수가 지키고 때 아니, synchronized키워드, 변수를 선언 중복 volatile, 그리고 혼란 @mrida을 피하기 위해를 방지하기 위해 권장
Enno시오 지

37

비록 당신을 요구 wait()하고 notify()특히,이 견적 여전히만큼 중요하다고 생각합니다 :

Josh Bloch, Effective Java 2nd Edition , 항목 69 : 동시성 유틸리티를 선호 wait하고 notify(강조) :

사용의 어려움을 감안 wait하고 notify정확하게, 대신 더 높은 수준의 동시성 유틸리티를 사용한다 [...]를 사용 wait하고 notify이 제공하는 높은 수준의 언어에 비해 직접하는 것은, "동시성 어셈블리 언어"로 프로그래밍 같다 java.util.concurrent. 이유 사용,면 적, 거의 없습니다 waitnotify새로운 코드 .


java.util.concurrent 패키지에 제공된 BlockingQueueS는 지속적이지 않습니다. 큐가 지속적이어야 할 때 무엇을 사용할 수 있습니까? 즉, 시스템이 대기열에 20 개의 항목이있는 경우 시스템이 다시 시작될 때 해당 항목이 있어야합니다. java.util.concurrent 대기열이 모두 '메모리에있는 것'으로 보이므로 지속성있는 구현을 제공하기 위해 그대로 / 해킹 / 재정의 된 방법으로 사용할 수 있습니까?
Volksman

1
아마도 백업 대기열이 제공 될 수 있습니까? 즉, 우리는 지속적인 대기열 인터페이스 구현을 제공 할 것입니다.
Volksman

이것은 당신이 사용할 필요가 없습니다 것이라고 이러한 맥락에서 언급에 매우 좋은 notify()는 AND를 wait()다시
Chaklader Asfak Arefe

7

Java Tutorial을 보셨습니까 ?

또한 실제 소프트웨어에서 이런 종류의 물건을 가지고 노는 것을 피하는 것이 좋습니다. 그것을 가지고 노는 것이 좋으므로 그것이 무엇인지 알 수 있지만 동시성은 모든 곳에서 함정을 가지고 있습니다. 다른 사람을위한 소프트웨어를 구축하는 경우 더 높은 수준의 추상화와 동기화 된 컬렉션 또는 JMS 대기열을 사용하는 것이 좋습니다.

그것은 적어도 내가하는 일입니다. 나는 동시성 전문가가 아니므로 가능한 한 손으로 스레드를 처리하는 것을 멀리합니다.


2

public class myThread extends Thread{
     @override
     public void run(){
        while(true){
           threadCondWait();// Circle waiting...
           //bla bla bla bla
        }
     }
     public synchronized void threadCondWait(){
        while(myCondition){
           wait();//Comminucate with notify()
        }
     }

}
public class myAnotherThread extends Thread{
     @override
     public void run(){
        //Bla Bla bla
        notify();//Trigger wait() Next Step
     }

}

0

스레딩의 wait () 및 notifyall ()의 예

동기화 된 정적 배열 목록은 리소스로 사용되며 배열 목록이 비어 있으면 wait () 메서드가 호출됩니다. notify () 메소드는 배열 목록에 요소가 추가되면 호출됩니다.

public class PrinterResource extends Thread{

//resource
public static List<String> arrayList = new ArrayList<String>();

public void addElement(String a){
    //System.out.println("Add element method "+this.getName());
    synchronized (arrayList) {
        arrayList.add(a);
        arrayList.notifyAll();
    }
}

public void removeElement(){
    //System.out.println("Remove element method  "+this.getName());
    synchronized (arrayList) {
        if(arrayList.size() == 0){
            try {
                arrayList.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }else{
            arrayList.remove(0);
        }
    }
}

public void run(){
    System.out.println("Thread name -- "+this.getName());
    if(!this.getName().equalsIgnoreCase("p4")){
        this.removeElement();
    }
    this.addElement("threads");

}

public static void main(String[] args) {
    PrinterResource p1 = new PrinterResource();
    p1.setName("p1");
    p1.start();

    PrinterResource p2 = new PrinterResource();
    p2.setName("p2");
    p2.start();


    PrinterResource p3 = new PrinterResource();
    p3.setName("p3");
    p3.start();


    PrinterResource p4 = new PrinterResource();
    p4.setName("p4");
    p4.start();     

    try{
        p1.join();
        p2.join();
        p3.join();
        p4.join();
    }catch(InterruptedException e){
        e.printStackTrace();
    }
    System.out.println("Final size of arraylist  "+arrayList.size());
   }
}

1
plz이 줄을 다시 확인 if(arrayList.size() == 0)하십시오. 여기서 실수 일 것 같습니다.
Wizmann
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.