如何在非线程对象上调用wait()和notify()方法?

50
如何在不是线程的对象上调用wait()notify()方法?这似乎没有意义,但是这两个方法对所有Java对象都是可用的。为什么呢?可以有人提供解释吗?我不太理解如何使用wait()notify()在线程之间通信。

6
任何“对象”都可以被用作监视器“对象”,因此“Object”类实现了这些方法。 - Reimeus
1
我认为它们在Object中的存在更像是一个“标记”,Thread继承自Object。 - Caffeinated
7
只有在同步方法或同步块内调用时,wait()notify()notifyAll()才是有效的。 - Eng.Fouad
10个回答

43

锁定是关于保护共享数据的。

锁定在被保护的数据结构上。线程是访问数据结构的对象。锁定在数据结构对象上,以防止线程以不安全的方式访问数据结构。

任何对象都可以用作内在锁定(意味着与synchronized一起使用)。这样,通过将同步修饰符添加到访问共享数据的方法中,您可以保护对任何对象的访问。

等待和通知方法是在用作锁的对象上调用的。锁是一个共享通信点:

  • 当持有锁的线程在其上调用notifyAll时,等待该锁的其他线程得到通知。当持有锁的线程在其上调用notify时,等待该锁的某个线程得到通知。

  • 当持有锁的线程在其上调用wait时,线程释放锁并进入休眠状态,直到a)它收到通知,或b)它仅因任意原因醒来(“虚假唤醒”);等待线程仍然停留在调用wait中,直到由于这两个原因之一而醒来,然后线程必须重新获取锁才能退出等待方法。

请参见Oracle有关保护块的教程,Drop类是共享数据结构,使用Producer和Consumer可运行对象的线程正在访问它。在Drop对象上锁定控制着线程如何访问Drop对象的数据。

JVM实现中使用线程作为锁定,建议应用程序开发人员避免使用线程作为锁定。例如,Thread.join的文档中说:

此实现使用基于 this.isAlive 的 this.wait 调用循环。当线程终止时,将调用 this.notifyAll 方法。建议应用程序不要在线程实例上使用 wait、notify 或 notifyAll 方法。

Java 5 引入了实现 java.util.concurrent.locks.Lock 的显式锁。这些锁比隐式锁更加灵活,有类似于 wait 和 notify(await 和 signal)的方法,但它们位于 Condition 上而不是锁上。具有多个条件使得可能只针对等待某种类型通知的那些线程进行操作。


谢谢您的解释,我有一个问题,为什么设计是这样的,即为什么wait、notify和notifyAll方法对于每个类都可用,因为每个类都有父类作为对象类,为什么不像cloneable接口一样需要重写克隆方法? - Rahul Singh
@Rahul:我不知道,但请记住Java最初是为小型设备上的移动代码而设计的。线程应该很容易实现,但他们没有考虑高并发服务器应用程序。 - Nathan Hughes
@NathanHughes “虽然这不是一个好主意,因为它允许可以访问该对象的任何线程获取其锁,即使它没有调用任何方法;最好将锁保留为被锁定的数据结构的私有成员,以便对其进行访问的限制。” 请让它更清晰明了。 - abksrv
@abksrv:有一个专门解决这个问题的问题,请查看http://stackoverflow/q/442564是否更清晰。 - Nathan Hughes
@NathanHughes 看起来链接已经失效了! - abksrv
@abksrv 避免使用synchronized this - Nathan Hughes

25

您可以使用wait()notify()来同步您的逻辑。例如:

synchronized (lock) {
    lock.wait(); // Will block until lock.notify() is called on another thread.
}

// Somewhere else...
...
synchronized (lock) {
    lock.notify(); // Will wake up lock.wait()
}

使用Object lock = new Object();定义为类成员变量的lock


4
这种东西的一个简单用法是消息生产者/消费者。当消费者使用consumer.wait()时,会一直等待,直到producer.notify()被调用。 - Christian Bongiorno
2
我认为这是最好的例子之一:http://www.javamex.com/tutorials/wait_notify_how_to.shtml - Alexander Mills
这不就是基本的锁吗? - shinzou

10

以一个现实生活的例子来思考,例如一个洗手间。当你想在办公室使用洗手间时,有两个选项可以确保在你使用它时没有其他人会进入洗手间。

  1. 锁上洗手间的门,这样其他人在尝试打开门时就知道已经有人在使用了。
  2. 去找办公室里每个人,锁住他们的座椅(或桌子,或其他什么东西),然后去使用洗手间。

你会选择哪个选项?

是的,在Java领域也是一样的!

所以在以上故事中,

  • 洗手间 = 你想要锁定的对象(只有你需要使用)
  • 你的同事 = 其他线程,你想要把它们排除在外

就像在现实生活中一样,当你有一些私人事务需要处理时,你会锁定该对象。当你完成该对象的使用时,你释放锁定!

(是的,这只是一个关于发生了什么的非常简单的描述。当然,实际概念与此略有不同,但这是一个起点)


5

使用静态Thread类方法sleep(),您可以按照需要停止线程的时间。

public class Main {
    //some code here

    //Thre thread will sleep for 5sec.
    Thread.sleep(5000);   
}

如果你想停止某些对象,你需要在 syncronized 块内调用这个方法。
public class Main {

//some code

public void waitObject(Object object) throws InterruptedException {
    synchronized(object) {
        object.wait();
    }
}

public void notifyObject(Object object) throws InterruptedException {
    synchronized(object) {
        object.notify();
    }
}

抱歉如果我误解了你的问题(英语不是我的母语)


1
谢谢,我喜欢这个解释:http://www.javamex.com/tutorials/wait_notify_how_to.shtml - Alexander Mills

4
当您将某些代码放在同步块内时:
 sychronized(lock){...}

一个想要执行此块内部操作的线程首先会获取对象上的锁,同一时间只有一个线程可以执行在相同对象上加锁的代码。任何对象都可以用作锁,但是您应该小心选择与作用域相关的对象。例如,当您有多个线程向帐户添加某些内容,并且它们都有负责在块内执行此操作的代码时:

sychronized(this){...}

如果它们都锁定了不同的对象,那么就不会发生同步。相反,您应该使用一个账户对象作为锁。 现在考虑这些线程还有从账户中提取的方法。在这种情况下,可能会出现一个线程想要提取某个金额但账户为空的情况。它应该等到有一些钱并释放锁以避免死锁。这就是等待和通知方法的用途。在这个例子中,遇到空账户的线程会释放锁并等待来自进行存款的某个线程的信号:

while(balance < amountToWithdraw){
    lock.wait();
}

当其他线程存入一些钱时,它会向正在等待同一锁的其他线程发出信号。(当然,负责存款和取款的代码必须在同一锁上同步才能使此方法可行,并防止数据损坏)。
balance += amountToDeposit;
lock.signallAll;

如您所见,wait和notify方法只有在synchronized块或方法内才有意义。


3
在Java中,所有的对象都实现了这两个方法。显然,如果没有监视器,这两个方法就没有意义。

3
实际上,waitnotify 成员函数不应该属于线程,它应该属于名为条件变量的东西,这个东西来自于posix线程,你可以看一下cpp是如何封装它的,它将其封装成了一个专用类std::condition_variable
Java没有做这种封装,而是以更高级的方式将条件变量包装在监视器中(直接把功能放到Object类中)。
如果你不知道监视器或条件变量,那么这确实会让人们在开始时感到困惑。

2
  1. 等待和通知不仅仅是普通的方法或同步工具,它们更是Java中两个线程之间的通信机制。如果没有任何Java关键字(例如synchronized)提供此机制,则Object类是使它们对每个对象可用的正确位置。请记住,synchronized和wait notify是两个不同的领域,不要混淆它们是相同或相关的。Synchronized是提供互斥和确保Java类的线程安全,例如竞态条件,而wait和notify是两个线程之间的通信机制。
  2. 锁在每个对象上可用,这是wait和notify声明在Object类而不是Thread类中的另一个原因。
  3. 在Java中,为了进入代码的临界区,线程需要锁定并等待锁定,它们不知道哪个线程持有锁定,而只知道锁定由某个线程持有,并且他们应该等待锁定,而不是知道哪个线程在同步块内并要求释放锁定。这个类比适用于在Java中将wait和notify放在Object类而不是Thread类中。

类比: Java线程是用户,厕所是线程希望执行的代码块。Java提供了一种方式来锁定当前执行它的线程的代码,使用synchorinized关键字,并使其他希望使用它的线程等待,直到第一个线程完成。这些其他线程处于等待状态。Java不像服务站那样公平,因为没有等待线程的队列。任何一个等待线程都可能下一步获得监视器,而不管它们请求它的顺序如何。唯一的保证是所有线程迟早都将使用监控代码。

来源

如果您查看以下生产者和消费者代码:
sharedQueue对象充当producer和consumer线程之间的线程间通信。

import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;

public class ProducerConsumerSolution {

    public static void main(String args[]) {
        Vector<Integer> sharedQueue = new Vector<Integer>();
        int size = 4;
        Thread prodThread = new Thread(new Producer(sharedQueue, size), "Producer");
        Thread consThread = new Thread(new Consumer(sharedQueue, size), "Consumer");
        prodThread.start();
        consThread.start();
    }
}

class Producer implements Runnable {

    private final Vector<Integer> sharedQueue;
    private final int SIZE;

    public Producer(Vector<Integer> sharedQueue, int size) {
        this.sharedQueue = sharedQueue;
        this.SIZE = size;
    }

    @Override
    public void run() {
        for (int i = 0; i < 7; i++) {
            System.out.println("Produced: " + i);
            try {
                produce(i);
            } catch (InterruptedException ex) {
                Logger.getLogger(Producer.class.getName()).log(Level.SEVERE, null, ex);
            }

        }
    }

    private void produce(int i) throws InterruptedException {

        // wait if queue is full
        while (sharedQueue.size() == SIZE) {
            synchronized (sharedQueue) {
                System.out.println("Queue is full " + Thread.currentThread().getName() + " is waiting , size: "
                        + sharedQueue.size());

                sharedQueue.wait();
            }
        }

        // producing element and notify consumers
        synchronized (sharedQueue) {
            sharedQueue.add(i);
            sharedQueue.notifyAll();
        }
    }
}

class Consumer implements Runnable {

    private final Vector<Integer> sharedQueue;
    private final int SIZE;

    public Consumer(Vector<Integer> sharedQueue, int size) {
        this.sharedQueue = sharedQueue;
        this.SIZE = size;
    }

    @Override
    public void run() {
        while (true) {
            try {
                System.out.println("Consumed: " + consume());
                Thread.sleep(50);
            } catch (InterruptedException ex) {
                Logger.getLogger(Consumer.class.getName()).log(Level.SEVERE, null, ex);
            }

        }
    }

    private int consume() throws InterruptedException {
        //wait if queue is empty
        while (sharedQueue.isEmpty()) {
            synchronized (sharedQueue) {
                System.out.println("Queue is empty " + Thread.currentThread().getName()
                                    + " is waiting , size: " + sharedQueue.size());

                sharedQueue.wait();
            }
        }

        //Otherwise consume element and notify waiting producer
        synchronized (sharedQueue) {
            sharedQueue.notifyAll();
            return (Integer) sharedQueue.remove(0);
        }
    }
}

Source


1
“这个方法只能被拥有该对象监视器的线程调用。” 因此,我认为您必须确保存在一个线程是该对象的监视器。

0

对象类是为每个对象提供锁定的正确位置。假设有一个联合银行账户,因此多个用户可以通过多个渠道使用同一账户进行交易。目前,该账户的余额为1500元,保持在账户中的最低金额为1000元。现在,第一个用户正在尝试通过ATM提取500元,另一个用户正在尝试通过刷卡机购买价值500元的商品。无论哪个渠道首先访问账户执行交易,都会首先在账户上获取锁定,而另一个渠道将等待直到交易完成并释放账户上的锁定,因为没有办法知道哪个渠道已经获取了锁定,哪个渠道正在等待获取锁定。因此,锁定始终应用于账户本身而不是渠道。在这里,我们可以将账户视为对象,将渠道视为线程。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接