在Java中使用wait()和notify()的简单场景


181

我可以得到一个完整的简单方案,即建议如何使用它的教程,特别是在队列中吗?

Answers:


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()一个线程实际阻塞之前,第二个线程的调用将无法进行。

其次,由于称为虚假唤醒的问题,您需要将要检查的条件放入while循环中,而不是if语句中。这是一个等待线程有时可以在不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仅唤醒一个线程。如果两个使用者线程竞争删除一个元素,则一个notify可能会唤醒另一个使用者线程,后者无法对其进行任何操作,然后会进入睡眠状态(而不是我们希望插入的新生产者插入一个新元素。)生产者线程未唤醒,没有任何插入,现在所有三个线程将无限期休眠。我删除了先前的评论,因为它(错误地)说是虚假唤醒是问题的原因(不是)
。– finnw

1
@finnw据我所知,您发现的问题可以通过使用notifyAll()来解决。我对吗?
克林特·伊斯特伍德

1
@Jared在此处给出的示例相当不错,但存在严重缺陷。在代码中,所有方法都标记为已同步,但是无法在同一时间执行两种同步方法,那么图片中的第二个线程又如何呢?
Shivam Aggarwal 2015年

10
@ Brut3Forc3您需要阅读wait()的javadoc:它说:该线程释放了此监视器的所有权。因此,一旦调用wait(),监视器便被释放,另一个线程可以执行队列的另一个同步方法。
JB Nizet 2015年

1
@JBNizet。“这样的例子是,当队列碰巧已满时,线程可以调用put(),然后检查条件,看队列是否已满,但是在它可以阻塞另一个线程之前被调度”。如果尚未调用wait,则计划第二个线程
Shivam Aggarwal 2015年

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(condition),因为

  • a)线程可能会零星地从等待状态中唤醒,而不会被任何人通知。(即使披萨小伙子没有敲响钟声,也会有人决定尝试吃披萨。)
  • b)获取同步锁后,应再次检查条件。假设比萨饼不会永远持续下去。您醒了,排队吃披萨,但对每个人来说还不够。如果不检查,可能会吃纸!:)(可能是更好的示例 while(!pizzaExists){ wait(); }

2)在调用等待/无效之前,您必须保持锁(同步)。线程也必须在唤醒之前获取锁。

3)尝试避免在同步块中获取任何锁,并努力不调用外来方法(您不确定它们在做什么的方法)。如果需要,请确保采取措施避免死锁。

4)注意notify()。坚持使用notifyAll(),直到您知道自己在做什么。

5)最后但并非最不重要的一点,请阅读实践Java并发


1
您能否详细说明为什么不使用“ if(!pizzaArrived){wait();}”?
大家

2
@Everyone:添加了一些解释。HTH。
Enno Shioji

1
为什么使用pizzaArrived标志?如果在没有调用的情况下更改了标志notify,则不会产生任何效果。也仅用于waitnotify调用示例即可。
Pablo Fernandez

2
我不明白-线程1执行eatPizza()方法并进入最上面的同步块,并在MyHouse类上进行同步。尚无披萨到达,因此等待。现在,线程2尝试通过调用pizzaGuy()方法交付披萨;但是不能,因为线程1已经拥有该锁并且它没有放弃它(它一直在等待)。实际上,结果是死锁-线程1等待线程2执行notifyAll()方法,而线程2等待线程1放弃对MyHouse类的锁定...我所缺少的是什么这里?
flamming_python 2014年

1
不,当变量由synchronized关键字保护时,声明该变量是多余的volatile,建议您避免使用该变量,以避免混淆@mrida
Enno Shioji

37

即使你要求wait()notify()具体而言,我觉得这句话仍然是足够重要:

Josh Bloch,Effective Java 2nd Edition,第69项:并发实用程序优先于waitnotify(强调他的):

考虑到使用waitnotify正确的困难,与相比,您应该使用更高级别的并发实用程序,而不是像使用“并发汇编语言”进行编程那样直接使用wait和。有很少,如果有的话,有理由使用,并在新的代码notifyjava.util.concurrentwaitnotify


java.util.concurrent包中提供的BlockingQueueS不是持久性的。当队列需要持久时,我们可以使用什么?即,如果系统出现故障,队列中有20个项目,那么我需要在系统重启时显示这些项目。由于java.util.concurrent队列似乎都只在“内存中”,是否有任何方法可以按原样/被黑/改写它们来提供具有持久性的实现?
大众队

1
也许可以提供支持队列?即我们将提供一个持久的Queue接口实现。
大众队

这是非常好的在这一背景下提到,你永远不会需要使用notify()的,并wait()再次
Chaklader Asfak Arefe

7

您看过此Java教程了吗?

此外,我建议您不要在真正的软件中玩这类东西。使用它很好,这样您就知道它是什么,但是并发性到处都是陷阱。如果要为其他人构建软件,最好使用更高级别的抽象和同步的集合或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

Threading中的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
请仔细检查这条线if(arrayList.size() == 0),我认为这可能是一个错误。
Wizmann '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.