สถานการณ์ง่าย ๆ โดยใช้การรอ () และการแจ้งเตือน () ในภาษาจาวา


181

ฉันจะได้รับสถานการณ์อย่างง่าย ๆ เช่นการสอนที่แนะนำว่าควรใช้สิ่งนี้อย่างไรกับคิวโดยเฉพาะ?

คำตอบ:


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()เรียกเธรดที่สองจะไม่สามารถดำเนินการได้จนกว่าเธรดแรกจะถูกบล็อกจริง

ประการที่สองคุณต้องใส่เงื่อนไขที่คุณกำลังตรวจสอบในขณะที่ห่วงแทนที่จะเป็นคำสั่งถ้าเนื่องจากปัญหาที่รู้จักกันเป็นเสแสร้งปลุก นี่คือตำแหน่งที่เธรดการรอสามารถเปิดใช้งานอีกครั้งโดยไม่notify()ถูกเรียก การวางการตรวจสอบนี้ในขณะที่ลูปจะช่วยให้มั่นใจได้ว่าหากมีการปลุกปลอมเกิดขึ้นเงื่อนไขจะถูกตรวจสอบอีกครั้งและเธรดจะเรียก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 ในทางปฏิบัติเนื่องจากครอบคลุมทุกสิ่งที่คุณต้องการทราบเกี่ยวกับปัญหาและวิธีแก้ไขที่เกี่ยวข้องกับการเกิดพร้อมกัน


7
@greuze notifyปลุกเพียงหนึ่งเธรด หากสองเธรดผู้บริโภคแข่งขันเพื่อลบองค์ประกอบหนึ่งการแจ้งเตือนอาจปลุกเธรดผู้บริโภคอื่นซึ่งไม่สามารถทำอะไรเกี่ยวกับมันและจะกลับไปนอน (แทนที่จะเป็นผู้ผลิตซึ่งเราหวังว่าจะแทรกองค์ประกอบใหม่) เพราะ เธรดโปรดิวเซอร์ไม่ได้ถูกปลุกไม่มีสิ่งใดแทรกและตอนนี้ทั้งสามเธรดจะนอนไม่ จำกัด ฉันลบความคิดเห็นก่อนหน้าของฉันตามที่กล่าวไว้ (ผิด) ว่าการปลุกลวงตาเป็นสาเหตุของปัญหา (ไม่ใช่)
finnw

1
@finnw เท่าที่ฉันสามารถบอกได้ว่าปัญหาที่คุณเห็นจะสามารถแก้ไขได้โดยใช้ alertAll () ฉันถูกไหม?
Clint Eastwood

1
ตัวอย่างที่ได้รับจาก @Jared นั้นค่อนข้างดี แต่มีการล้มลงอย่างรุนแรง ในรหัสวิธีการทั้งหมดได้รับการทำเครื่องหมายว่าซิงโครไนซ์ แต่ไม่มีวิธีการซิงโครไนซ์สองวิธีที่สามารถดำเนินการได้ในเวลาเดียวกันจากนั้นจึงมีกระทู้ที่สองในภาพ
Shivam Aggarwal

10
@ Brut3Forc3 คุณต้องอ่าน javadoc ของการรอ (): มันบอกว่า: เธรดจะปล่อยความเป็นเจ้าของของจอภาพนี้ ดังนั้นทันทีที่รอ () ถูกเรียกใช้มอนิเตอร์จะถูกปล่อยและเธรดอื่นสามารถดำเนินการวิธีการซิงโครไนซ์อื่นของคิว
JB Nizet

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();
 }

ใช้ในขณะที่ (เงื่อนไข) เสมอเพราะ

  • ก) กระทู้สามารถตื่นขึ้นมาเป็นระยะ ๆ จากสถานะรอโดยไม่ได้รับการแจ้งเตือนจากใคร (แม้เมื่อคนพิซซ่าไม่ได้ใส่เสียงเรียกเข้าใครก็ตัดสินใจลองทานพิซซ่า)
  • b) คุณควรตรวจสอบเงื่อนไขอีกครั้งหลังจากได้รับการล็อคแบบซิงโครไนซ์ สมมติว่าพิซซ่าไม่คงอยู่ตลอดไป คุณตื่นสายขึ้นสำหรับพิซซ่า แต่มันก็ไม่เพียงพอสำหรับทุกคน หากคุณไม่ตรวจสอบคุณอาจกินกระดาษ! :) while(!pizzaExists){ wait(); }(ตัวอย่างเช่นอาจจะดีกว่าที่จะเป็น

2) คุณต้องระงับการล็อค (ซิงโครไนซ์) ก่อนที่จะเรียกใช้ wait / nofity หัวข้อต้องได้รับล็อคก่อนที่จะตื่น

3) พยายามหลีกเลี่ยงการได้รับการล็อคใด ๆ ภายในบล็อกที่ซิงโครไนซ์ของคุณและพยายามอย่าเรียกใช้วิธีการต่างดาว (วิธีที่คุณไม่รู้ว่ามันกำลังทำอะไรอยู่) หากคุณต้องตรวจสอบให้แน่ใจว่าได้ใช้มาตรการเพื่อหลีกเลี่ยงการหยุดชะงัก

4) ระมัดระวังด้วยการแจ้งเตือน () ติดกับ INFORMAll () จนกว่าคุณจะรู้ว่าคุณกำลังทำอะไรอยู่

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 เพื่อเรียกใช้เมธอด allowAll () ในขณะที่เธรด 2 กำลังรอเธรด 1 เพื่อยกเลิกการล็อกบนคลาส MyHouse ... ฉันหายไปอะไร ที่นี่?
flamming_python

1
ไม่เมื่อตัวแปรได้รับการปกป้องด้วยsynchronizedคีย์เวิร์ดจะมีการซ้ำซ้อนเพื่อประกาศตัวแปรvolatileและแนะนำให้หลีกเลี่ยงความสับสน @mrida
Enno Shioji

37

แม้ว่าคุณจะขอwait()และnotify()เจาะจง แต่ฉันรู้สึกว่าคำพูดนี้ยังมีความสำคัญพอ:

Josh Bloch, Java รุ่นที่ 2 ที่มีประสิทธิภาพ , รายการ 69: แนะนำยูทิลิตี้การทำงานพร้อมกันให้waitและnotify(เน้นที่เขา):

ได้รับความยากลำบากของการใช้waitและnotifyถูกต้องคุณควรใช้ระดับสูงสาธารณูปโภคเห็นพ้องแทน [ ... ] โดยใช้waitและnotifyโดยตรงเป็นเหมือนการเขียนโปรแกรมใน "ภาษาประกอบการเห็นพ้อง" java.util.concurrentเมื่อเทียบกับภาษาระดับสูงให้บริการโดย ไม่ค่อยมีหากเคยมีเหตุผลที่จะใช้waitและnotifyในรหัสใหม่


BlockingQueueS ที่ให้ไว้ในแพ็คเกจ java.util.concurrent จะไม่คงอยู่ เราสามารถใช้อะไรได้บ้างเมื่อคิวต้องคงอยู่? เช่นถ้าระบบหยุดทำงาน 20 รายการในคิวฉันต้องการรายการเหล่านั้นเมื่อระบบรีสตาร์ท ในฐานะที่เป็นคิว java.util.concurrent ทั้งหมดดูเหมือนจะเป็น 'ในหน่วยความจำ' เท่านั้นมีวิธีใด ๆ เหล่านี้สามารถใช้เป็น / แฮ็ค / แทนที่เพื่อให้การใช้งานที่มีความสามารถคงอยู่?
Volksman

1
บางทีอาจมีคิวการสำรองข้อมูล เช่นเราจะจัดให้มีการใช้อินเตอร์เฟสคิวที่คงอยู่
Volksman

นี่เป็นเรื่องดีมากที่จะกล่าวถึงในบริบทนี้ว่าคุณไม่จำเป็นต้องใช้notify()และwait()อีกต่อไป
Chaklader Asfak Arefe

7

คุณดูที่บทช่วยสอนนี้แล้วหรือยัง

นอกจากนี้ฉันขอแนะนำให้คุณหลีกเลี่ยงการเล่นกับสิ่งต่าง ๆ ในซอฟต์แวร์จริง มันดีที่จะเล่นกับมันเพื่อที่คุณจะได้รู้ว่ามันคืออะไร แต่การเกิดขึ้นพร้อมกันนั้นมีข้อผิดพลาดทั่วทุกที่ เป็นการดีกว่าที่จะใช้ abstractions ระดับสูงขึ้นและคอลเลกชันที่ถูกซิงโครไนซ์หรือคิว 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

ตัวอย่างการรอ () และ alertall () ในเธรด

รายการอาเรย์แบบคงที่ที่ซิงโครไนซ์จะใช้เป็นทรัพยากรและเมธอด wait () จะถูกเรียกใช้ถ้ารายการอาเรย์นั้นว่างเปล่า วิธีการแจ้งเตือน () ถูกเรียกเมื่อองค์ประกอบถูกเพิ่มสำหรับรายการอาร์เรย์

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
โปรดตรวจสอบสายนี้if(arrayList.size() == 0)อีกฉันคิดว่ามันอาจจะผิดพลาด
Wizmann
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.