ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
# 如何在 Java 中使用`wait()`,`notify()`和`notifyAll()`? > 原文: [https://howtodoinjava.com/java/multi-threading/wait-notify-and-notifyall-methods/](https://howtodoinjava.com/java/multi-threading/wait-notify-and-notifyall-methods/) [**Java 并发**](https://howtodoinjava.com/java-concurrency-tutorial/ "multithreading in java")是一个非常复杂的主题,在编写处理在任何给定时间访问一个/多个共享资源的多个线程的应用程序代码时,需要引起很多关注。 Java 5 引入了诸如[**`BlockingQueue`**](https://howtodoinjava.com/java/multi-threading/how-to-use-blockingqueue-and-threadpoolexecutor-in-java/ "How to use BlockingQueue and ThreadPoolExecutor in java")和`Executors`之类的类,它们通过提供易于使用的 API 消除了一些复杂性。 使用并发类的程序员会比使用`wait()`,`notify()`和`notifyAll()`方法调用直接处理同步内容的程序员更有信心。 我还将建议您自己通过同步使用这些较新的 API,但是由于种种原因,我们经常需要这样做,例如维护旧版代码。 掌握这些方法的丰富知识将在您到达这种情况时为您提供帮助。 在本教程中,我将讨论 Java 中`wait() notify() notifyall()`的用途。 我们将了解`wait`和`notify`之间的**区别**。 > 阅读更多: [Java 中的`wait()`和`sleep()`之间的区别](https://howtodoinjava.com/java/multi-threading/difference-between-sleep-and-wait/) ## 1\. 什么是`wait()`,`notify()`和`notifyAll()`方法? Java 中的`Object`类具有三个最终方法,这些方法允许线程就资源的锁定状态进行通信。 1. #### `wait()` 它告诉调用线程放弃锁定并进入睡眠状态,直到其他线程进入同一监视器并调用`notify()`为止。 `wait()`方法在等待之前释放锁,并在从`wait()`方法返回之前重新获取锁。 实际上,`wait()`方法与同步锁紧密集成在一起,使用的特性无法直接从同步机制获得。 换句话说,我们不可能仅在 Java 中实现`wait()`方法。 它是**本地方法**。 调用`wait()`方法的常规语法如下: ```java synchronized( lockObject ) { while( ! condition ) { lockObject.wait(); } //take the action here; } ``` 2. #### `notify()` 它唤醒一个在同一对象上调用`wait()`的单个线程。 应该注意的是,调用`notify()`实际上并没有放弃对资源的锁定。 它告诉等待的线程该线程可以唤醒。 但是,直到通知者的同步块完成后才真正放弃锁定。 因此,如果通知者在资源上调用`notify()`,但通知者仍需要在其同步块内对该资源执行 10 秒的操作,则一直在等待的线程将至少需要再等待通知者 10 秒,来释放对象上的锁定,即使调用了`notify()`。 调用`notify()`方法的常规语法如下: ```java synchronized(lockObject) { //establish_the_condition; lockObject.notify(); //any additional code if needed } ``` 3. #### `notifyAll()` 它将唤醒在同一对象上调用`wait()`的所有线程。 尽管没有保证,但是在大多数情况下,优先级最高的线程将首先运行。 其他与上述`notify()`方法相同。 调用`notify()`方法的一般语法如下: ```java synchronized(lockObject) { establish_the_condition; lockObject.notifyAll(); } ``` 通常,使用`wait()`方法的线程会确认条件不存在(通常通过检查变量),然后调用`wait()`方法。 当另一个线程建立条件(通常通过设置相同的变量)时,它将调用`notify()`方法。 等待通知机制未指定特定条件/变量值是什么。 开发人员可以在调用`wait()`或`notify()`之前指定要检查的条件。 让我们写一个小程序来了解**应该如何使用**`wait()`,`notify()`和`notifyall()`方法来获得理想的结果。 ## 2\. 如何一起使用`wait()`,`notify()`和`notifyAll()`方法 在本练习中,我们将使用`wait()`和`notify()`方法解决**生产者消费者问题**。 为了使程序简单并专注于`wait()`和`notify()`方法的使用,我们将只涉及一个生产者和一个消费者线程。 该程序的其他特性包括: * 生产者线程每 1 秒钟产生一个新资源,并将其放入`taskQueue`中。 * 使用者线程需要 1 秒钟来处理`taskQueue`中消耗的资源。 * `taskQueue`的最大容量为 5,即在任何给定时间,`taskQueue`中最多可以存在 5 个资源。 * 两个线程都无限运行。 #### 2.1 生产者线程 以下是根据我们的要求生产者线程的代码: ```java class Producer implements Runnable { private final List<Integer> taskQueue; private final int MAX_CAPACITY; public Producer(List<Integer> sharedQueue, int size) { this.taskQueue = sharedQueue; this.MAX_CAPACITY = size; } @Override public void run() { int counter = 0; while (true) { try { produce(counter++); } catch (InterruptedException ex) { ex.printStackTrace(); } } } private void produce(int i) throws InterruptedException { synchronized (taskQueue) { while (taskQueue.size() == MAX_CAPACITY) { System.out.println("Queue is full " + Thread.currentThread().getName() + " is waiting , size: " + taskQueue.size()); taskQueue.wait(); } Thread.sleep(1000); taskQueue.add(i); System.out.println("Produced: " + i); taskQueue.notifyAll(); } } } ``` * 此处,`produce(counter++)`代码已在无限循环内编写,以便生产者以规则的间隔保持生产元素。 * 我们已经按照通用准则编写了`produce()`方法代码,以编写第一部分中提到的`wait()`方法。 * `wait()`完成后,生产者在 taskQueue 中添加一个元素,并称为`notifyAll()`方法。 由于上次`wait()`方法是由使用者线程调用的(这就是生产者处于等待状态的原因),因此使用者可以获取通知。 * 收到通知后的使用者线程,如果准备按照书面逻辑使用该元素。 * 请注意,两个线程也都使用`sleep()`方法来模拟创建和使用元素时的时间延迟。 #### 2.2 使用者线程 以下是根据我们的要求使用的消费者线程的代码: ```java class Consumer implements Runnable { private final List<Integer> taskQueue; public Consumer(List<Integer> sharedQueue) { this.taskQueue = sharedQueue; } @Override public void run() { while (true) { try { consume(); } catch (InterruptedException ex) { ex.printStackTrace(); } } } private void consume() throws InterruptedException { synchronized (taskQueue) { while (taskQueue.isEmpty()) { System.out.println("Queue is empty " + Thread.currentThread().getName() + " is waiting , size: " + taskQueue.size()); taskQueue.wait(); } Thread.sleep(1000); int i = (Integer) taskQueue.remove(0); System.out.println("Consumed: " + i); taskQueue.notifyAll(); } } } ``` * 此处,`consume()`代码已在无限循环内编写,以便使用者在`taskQueue`中发现某些内容时便继续使用元素。 * 一旦`wait()`完成,使用者将删除 taskQueue 中的一个元素,并调用`notifyAll()`方法。 由于生产者线程调用了上次的`wait()`方法(这就是为什么生产者处于等待状态的原因),所以生产者会收到通知。 * 获取通知后的生产者线程(如果准备按照书面逻辑生产元素)。 #### 2.3 测试生产者消费者示例 现在让测试生产者和使用者线程。 ```java public class ProducerConsumerExampleWithWaitAndNotify { public static void main(String[] args) { List<Integer> taskQueue = new ArrayList<Integer>(); int MAX_CAPACITY = 5; Thread tProducer = new Thread(new Producer(taskQueue, MAX_CAPACITY), "Producer"); Thread tConsumer = new Thread(new Consumer(taskQueue), "Consumer"); tProducer.start(); tConsumer.start(); } } ``` 程序输出。 ```java Produced: 0 Consumed: 0 Queue is empty Consumer is waiting , size: 0 Produced: 1 Produced: 2 Consumed: 1 Consumed: 2 Queue is empty Consumer is waiting , size: 0 Produced: 3 Produced: 4 Consumed: 3 Produced: 5 Consumed: 4 Produced: 6 Consumed: 5 Consumed: 6 Queue is empty Consumer is waiting , size: 0 Produced: 7 Consumed: 7 Queue is empty Consumer is waiting , size: 0 ``` 我建议您将生产者线程和使用者线程花费的时间更改为不同的时间,并检查不同情况下的不同输出。 ## 3\. 关于`wait()`,`notify()`和`notifyAll()`方法的面试问题 #### 3.1 调用`notify()`并且没有线程正在等待时会发生什么? 通常,如果正确使用这些方法,则在大多数情况下并非如此。 尽管如果在没有其他线程等待时调用`notify()`方法,则`notify()`只会返回而通知将丢失。 由于*等待和通知机制*不知道它正在发送通知的条件,因此,假设没有线程正在等待,通知就不会被听到。 稍后执行`wait()`方法的线程必须等待另一个通知发生。 #### 3.2 在`wait()`方法释放或重新获取锁的时间段内是否存在竞争条件? `wait()`方法与锁定机制紧密集成。 在等待线程已经可以接收通知的状态之前,实际上不会释放对象锁。 这意味着仅当线程状态更改为能够接收通知时,才会保留锁定。 该系统可防止在此机制中发生任何竞赛情况。 同样,系统确保在将线程移出等待状态之前,对象应完全持有锁定。 #### 3.3 如果线程收到通知,是否可以保证条件设置正确? 简单地说,不。 在调用`wait()`方法之前,线程应始终在保持同步锁的同时测试条件。 从`wait()`方法返回后,线程应始终重新测试条件以确定是否应该再次等待。 这是因为另一个线程也可以测试条件并确定不需要等待 - 处理通知线程设置的有效数据。 当通知中涉及多个线程时,这是一种常见情况。 更具体地说,正在将处理数据的线程视为使用者。 它们使用其他线程产生的数据。 无法保证当消费者收到通知时,该通知尚未被其他消费者处理。 这样,当消费者醒来时,它无法假定其等待的状态仍然有效。 它可能在过去是有效的,但是在调用`notify()`方法之后以及使用者线程唤醒之前,状态可能已经更改。 等待线程必须提供检查状态的选项,并在通知已被处理的情况下返回到等待状态。 这就是为什么我们总是在循环中放置对`wait()`方法的调用的原因。 #### 3.4 当多个线程正在等待通知时会发生什么? 调用`notify()`方法时,哪个线程实际获得通知? 这取决于许多因素。Java 规范没有定义要通知哪个线程。 在运行时中,哪个线程实际接收到通知取决于几个因素,包括 Java 虚拟机的实现以及程序执行期间的调度和计时问题。 即使在单个处理器平台上,也无法确定多个线程中的哪个接收通知。 就像`notify()`方法一样,`notifyAll()`方法不允许我们决定哪个线程获取通知:它们都被通知了。 当所有线程都收到通知时,可以设计出一种机制,让线程在它们之间选择哪个线程应该继续,哪个线程应该再次调用`wait()`方法。 #### 3.5 `notifyAll()`方法是否真的唤醒所有线程? 是的,没有。 所有等待的线程都被唤醒,但是它们仍然必须重新获取对象锁。 因此,线程不会并行运行:它们必须各自等待对象锁被释放。 因此,一次只能运行一个线程,并且只能在调用`notifyAll()`方法的线程释放其锁之后运行。 #### 3.6 如果根本只执行一个线程,为什么要唤醒所有线程? 有几个原因。 例如,可能有多个条件要等待。 由于我们无法控制哪个线程获取通知,因此通知完全有可能唤醒正在等待完全不同条件的线程。 通过唤醒所有线程,我们可以设计程序,以便线程在它们之间决定下一步应执行哪个线程。 另一种选择是生产者生成的数据可以满足多个消费者的需求。 由于可能难以确定有多少消费者可以对该通知感到满意,因此可以选择全部通知他们,从而允许消费者在他们之间进行分类。 学习愉快!