如何在 Java 中使用wait()
,notify()
和notifyAll()
?
原文: https://howtodoinjava.com/java/multi-threading/wait-notify-and-notifyall-methods/
Java 并发是一个非常复杂的主题,在编写处理在任何给定时间访问一个/多个共享资源的多个线程的应用程序代码时,需要引起很多关注。 Java 5 引入了诸如BlockingQueue
和Executors
之类的类,它们通过提供易于使用的 API 消除了一些复杂性。
使用并发类的程序员会比使用wait()
,notify()
和notifyAll()
方法调用直接处理同步内容的程序员更有信心。 我还将建议您自己通过同步使用这些较新的 API,但是由于种种原因,我们经常需要这样做,例如维护旧版代码。 掌握这些方法的丰富知识将在您到达这种情况时为您提供帮助。
在本教程中,我将讨论 Java 中wait() notify() notifyall()
的用途。 我们将了解wait
和notify
之间的区别。
1. 什么是wait()
,notify()
和notifyAll()
方法?
Java 中的Object
类具有三个最终方法,这些方法允许线程就资源的锁定状态进行通信。
-
wait()
它告诉调用线程放弃锁定并进入睡眠状态,直到其他线程进入同一监视器并调用
notify()
为止。wait()
方法在等待之前释放锁,并在从wait()
方法返回之前重新获取锁。 实际上,wait()
方法与同步锁紧密集成在一起,使用的特性无法直接从同步机制获得。换句话说,我们不可能仅在 Java 中实现
wait()
方法。 它是本地方法。调用
wait()
方法的常规语法如下:synchronized( lockObject ) { while( ! condition ) { lockObject.wait(); } //take the action here; }
-
notify()
它唤醒一个在同一对象上调用
wait()
的单个线程。 应该注意的是,调用notify()
实际上并没有放弃对资源的锁定。 它告诉等待的线程该线程可以唤醒。 但是,直到通知者的同步块完成后才真正放弃锁定。因此,如果通知者在资源上调用
notify()
,但通知者仍需要在其同步块内对该资源执行 10 秒的操作,则一直在等待的线程将至少需要再等待通知者 10 秒,来释放对象上的锁定,即使调用了notify()
。调用
notify()
方法的常规语法如下:synchronized(lockObject) { //establish_the_condition; lockObject.notify(); //any additional code if needed }
-
notifyAll()
它将唤醒在同一对象上调用
wait()
的所有线程。 尽管没有保证,但是在大多数情况下,优先级最高的线程将首先运行。 其他与上述notify()
方法相同。调用
notify()
方法的一般语法如下: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 生产者线程
以下是根据我们的要求生产者线程的代码:
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 使用者线程
以下是根据我们的要求使用的消费者线程的代码:
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 测试生产者消费者示例
现在让测试生产者和使用者线程。
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();
}
}
程序输出。
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 如果根本只执行一个线程,为什么要唤醒所有线程?
有几个原因。 例如,可能有多个条件要等待。 由于我们无法控制哪个线程获取通知,因此通知完全有可能唤醒正在等待完全不同条件的线程。
通过唤醒所有线程,我们可以设计程序,以便线程在它们之间决定下一步应执行哪个线程。 另一种选择是生产者生成的数据可以满足多个消费者的需求。 由于可能难以确定有多少消费者可以对该通知感到满意,因此可以选择全部通知他们,从而允许消费者在他们之间进行分类。
学习愉快!