Một kịch bản đơn giản sử dụng Wait () và notify () trong java


181

Tôi có thể có được một kịch bản đơn giản hoàn chỉnh tức là hướng dẫn gợi ý cách sử dụng cái này, cụ thể là với Hàng đợi không?

Câu trả lời:


269

Các phương thức wait()notify()được thiết kế để cung cấp một cơ chế cho phép chặn một luồng cho đến khi một điều kiện cụ thể được đáp ứng. Đối với điều này, tôi giả sử bạn muốn viết một triển khai hàng đợi chặn, nơi bạn có một số phần tử dự phòng có kích thước cố định.

Điều đầu tiên bạn phải làm là xác định các điều kiện mà bạn muốn các phương thức chờ đợi. Trong trường hợp này, bạn sẽ muốn put()phương thức chặn cho đến khi có không gian trống trong cửa hàng và bạn sẽ muốn take()phương thức chặn cho đến khi có một phần tử nào đó quay trở lại.

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

Có một vài điều cần lưu ý về cách mà bạn phải sử dụng các cơ chế chờ và thông báo.

Trước tiên, bạn cần đảm bảo rằng mọi cuộc gọi đến wait()hoặc notify()nằm trong một vùng mã được đồng bộ hóa (với các cuộc gọi wait()notify()được đồng bộ hóa trên cùng một đối tượng). Lý do cho điều này (ngoài các mối quan tâm an toàn luồng tiêu chuẩn) là do một cái gì đó được gọi là tín hiệu bị bỏ lỡ.

Một ví dụ về điều này, là một luồng có thể gọi put()khi hàng đợi xảy ra đầy, sau đó nó kiểm tra điều kiện, thấy rằng hàng đợi đã đầy, tuy nhiên trước khi nó có thể chặn một luồng khác được lên lịch. Chuỗi thứ hai này sau đó take()là một phần tử từ hàng đợi và thông báo cho các luồng chờ rằng hàng đợi không còn đầy đủ. Vì luồng đầu tiên đã kiểm tra điều kiện, tuy nhiên, nó sẽ chỉ gọi wait()sau khi được lên lịch lại, mặc dù nó có thể đạt được tiến bộ.

Bằng cách đồng bộ hóa trên một đối tượng được chia sẻ, bạn có thể đảm bảo rằng sự cố này không xảy ra, vì take()cuộc gọi của luồng thứ hai sẽ không thể thực hiện tiến trình cho đến khi luồng đầu tiên thực sự bị chặn.

Thứ hai, bạn cần đặt điều kiện bạn đang kiểm tra trong một vòng lặp while, thay vì một câu lệnh if, do một vấn đề được gọi là đánh thức giả. Đây là nơi một luồng chờ đôi khi có thể được kích hoạt lại mà không notify()được gọi. Đặt kiểm tra này trong một vòng lặp while sẽ đảm bảo rằng nếu xảy ra đánh thức giả, điều kiện sẽ được kiểm tra lại và luồng sẽ gọi wait()lại.


Như một số câu trả lời khác đã đề cập, Java 1.5 đã giới thiệu một thư viện đồng thời mới (trong java.util.concurrentgói) được thiết kế để cung cấp một mức độ trừu tượng cao hơn so với cơ chế chờ / thông báo. Sử dụng các tính năng mới này, bạn có thể viết lại ví dụ ban đầu như vậy:

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

Tất nhiên, nếu bạn thực sự cần một hàng đợi chặn, thì bạn nên sử dụng một triển khai của giao diện BlockingQueue .

Ngoài ra, đối với những thứ như thế này tôi rất khuyến nghị Java đồng thời trong thực tiễn , vì nó bao gồm mọi thứ bạn có thể muốn biết về các vấn đề và giải pháp liên quan đến tương tranh.


7
@greuze, notifychỉ đánh thức một chủ đề. Nếu hai luồng tiêu dùng đang cạnh tranh để loại bỏ một yếu tố, một thông báo có thể đánh thức luồng tiêu dùng khác, không thể làm gì về nó và sẽ quay trở lại trạng thái ngủ (thay vì nhà sản xuất, mà chúng tôi hy vọng sẽ chèn một yếu tố mới.) Bởi vì chủ đề của nhà sản xuất không được đánh thức, không có gì được chèn vào và bây giờ cả ba chủ đề sẽ ngủ vô thời hạn. Tôi đã xóa nhận xét trước đây của mình như đã nói (sai) rằng đánh thức giả là nguyên nhân của vấn đề (Không phải vậy.)
finnw 17/213

1
@finnw Theo như tôi có thể nói, vấn đề mà bạn đã phát hiện ra có thể được giải quyết bằng cách sử dụng notifyAll (). Tôi có đúng không
Clint Eastwood

1
Ví dụ được đưa ra ở đây bởi @Jared là khá tốt nhưng đã giảm nghiêm trọng. Trong mã, tất cả các phương thức đã được đánh dấu là đồng bộ hóa, nhưng KHÔNG CÓ HAI PHƯƠNG PHÁP TỔNG HỢP NÀO CÓ THỂ ĐƯỢC THỰC HIỆN TRONG THỜI GIAN, sau đó làm thế nào có luồng thứ hai trong hình.
Shivam Aggarwal

10
@ Brut3Forc3 bạn cần đọc javadoc of Wait (): nó nói: Chủ đề phát hành quyền sở hữu màn hình này . Vì vậy, ngay sau khi Wait () được gọi, màn hình được giải phóng và một luồng khác có thể thực thi một phương thức đồng bộ hóa khác của hàng đợi.
JB Nizet

1
@JBNizet. "Một ví dụ về điều này, là một luồng có thể gọi put () khi hàng đợi xảy ra đầy, sau đó kiểm tra điều kiện, thấy rằng hàng đợi đã đầy, tuy nhiên trước khi nó có thể chặn một luồng khác được lên lịch. chuỗi thứ hai được lên lịch nếu chờ đợi chưa được gọi.
Shivam Aggarwal

148

Không phải là một ví dụ xếp hàng, nhưng cực kỳ đơn giản :)

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

Một số điểm quan trọng:
1) KHÔNG BAO GIỜ làm

 if(!pizzaArrived){
     wait();
 }

Luôn luôn sử dụng while (điều kiện), bởi vì

  • a) chủ đề có thể thức tỉnh lẻ tẻ từ trạng thái chờ mà không được ai thông báo. (ngay cả khi anh chàng pizza không bấm chuông, ai đó sẽ quyết định thử ăn pizza.).
  • b) Bạn nên kiểm tra lại tình trạng sau khi có được khóa đồng bộ. Hãy nói rằng pizza không tồn tại mãi mãi. Bạn thức dậy, xếp hàng cho pizza, nhưng nó không đủ cho tất cả mọi người. Nếu bạn không kiểm tra, bạn có thể ăn giấy! :) (có lẽ ví dụ tốt hơn sẽ là while(!pizzaExists){ wait(); }.

2) Bạn phải giữ khóa (được đồng bộ hóa) trước khi gọi chờ / nofity. Chủ đề cũng phải có được khóa trước khi thức dậy.

3) Cố gắng tránh mua bất kỳ khóa nào trong khối được đồng bộ hóa của bạn và cố gắng không gọi các phương thức của người ngoài hành tinh (các phương pháp bạn không biết chắc chắn họ đang làm gì). Nếu bạn phải, hãy chắc chắn thực hiện các biện pháp để tránh bế tắc.

4) Cẩn thận với thông báo (). Gắn bó với notifyAll () cho đến khi bạn biết bạn đang làm gì.

5) Cuối cùng, nhưng không kém phần quan trọng, hãy đọc Đồng thời Java trong Thực tiễn !


1
Bạn có thể giải thích rõ hơn về lý do tại sao không sử dụng "if (! PizzaArrived) {Wait ();}" không?
Mọi người

2
@Everyone: Đã thêm một số lời giải thích. HTH.
Enno Shioji

1
Tại sao nên sử dụng pizzaArrivedcờ? nếu cờ được thay đổi mà không có lệnh gọi notifythì nó sẽ không có hiệu lực. Cũng chỉ với waitnotifygọi các ví dụ hoạt động.
Pablo Fernandez

2
Tôi không hiểu - luồng 1 thực thi phương thức eatPizza () và nhập vào khối được đồng bộ hóa hàng đầu và đồng bộ hóa trên lớp MyHouse. Chưa có bánh pizza nào đến nên nó chỉ chờ. Bây giờ, luồng 2 cố gắng phân phối pizza bằng cách gọi phương thức pizzaGuy (); nhưng không thể vì luồng 1 đã sở hữu khóa và nó không từ bỏ (nó luôn chờ đợi). Kết quả là sự bế tắc - luồng 1 đang chờ luồng 2 thực thi phương thức notifyAll (), trong khi luồng 2 đang chờ luồng 1 từ bỏ khóa trên lớp MyHouse ... Tôi đang thiếu gì đây?
flamming_python

1
Không, khi một biến được bảo vệ bởi synchronizedtừ khóa, việc khai báo biến đó là không cần thiết volatilevà nên tránh nó để tránh nhầm lẫn @mrida
Enno Shioji

37

Mặc dù bạn đã yêu cầu wait()notify()đặc biệt, tôi cảm thấy rằng trích dẫn này vẫn đủ quan trọng:

Josh Bloch, Phiên bản Java hiệu quả thứ 2 , Mục 69: Thích các tiện ích đồng thời hơn waitnotify(nhấn mạnh vào nó):

Do khó sử dụng waitnotifychính xác, bạn nên sử dụng các tiện ích đồng thời cấp cao hơn thay vì [...] sử dụng waitnotifytrực tiếp giống như lập trình trong "ngôn ngữ lắp ráp đồng thời", so với ngôn ngữ cấp cao hơn được cung cấp bởi java.util.concurrent. Nếu hiếm khi, lý do để sử dụng waitnotifytrong mã mới .


BlockingQueueS được cung cấp trong gói java.util.conc hiện không tồn tại. Chúng ta có thể sử dụng gì khi hàng đợi cần phải kiên trì? tức là nếu hệ thống ngừng hoạt động với 20 mục trong hàng đợi thì tôi cần những thứ đó để có mặt khi hệ thống khởi động lại. Vì tất cả các hàng đợi java.util.conc hiện tại chỉ là 'trong bộ nhớ', có cách nào chúng có thể được sử dụng như là bị hack / bị ghi đè để cung cấp các triển khai có khả năng tồn tại không?
Volksman

1
Có lẽ hàng đợi ủng hộ có thể được cung cấp? tức là chúng tôi sẽ cung cấp một triển khai giao diện Hàng đợi liên tục.
Volksman

Điều này rất tốt để đề cập trong bối cảnh này mà bạn sẽ không bao giờ cần phải sử dụng notify()wait()một lần nữa
Chaklader Asfak Arefe

7

Bạn đã xem Hướng dẫn Java này chưa?

Hơn nữa, tôi khuyên bạn nên tránh xa việc chơi với loại công cụ này trong phần mềm thực. Chơi với nó thật tốt để bạn biết nó là gì, nhưng đồng thời có những cạm bẫy ở khắp mọi nơi. Tốt hơn là sử dụng trừu tượng hóa cấp cao hơn và các bộ sưu tập được đồng bộ hóa hoặc hàng đợi JMS nếu bạn đang xây dựng phần mềm cho người khác.

Đó là ít nhất những gì tôi làm. Tôi không phải là một chuyên gia đồng thời vì vậy tôi tránh xa việc xử lý các luồng bằng tay bất cứ khi nào có thể.


2

Thí dụ

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

Ví dụ cho Wait () và notifyall () trong Threading.

Một danh sách mảng tĩnh được đồng bộ hóa được sử dụng làm tài nguyên và phương thức Wait () được gọi nếu danh sách mảng trống. Phương thức notify () được gọi sau khi một phần tử được thêm vào danh sách mảng.

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
Xin hãy kiểm tra lại dòng này if(arrayList.size() == 0), tôi nghĩ nó có thể là một lỗi ở đây.
Wizmann 2/11/2016
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.