如何在 Java 中使用wait(),notify()和notifyAll()?

如何在 Java 中使用wait()notify()notifyAll()

原文: https://howtodoinjava.com/java/multi-threading/wait-notify-and-notifyall-methods/

Java 并发是一个非常复杂的主题,在编写处理在任何给定时间访问一个/多个共享资源的多个线程的应用程序代码时,需要引起很多关注。 Java 5 引入了诸如BlockingQueueExecutors之类的类,它们通过提供易于使用的 API 消除了一些复杂性。

使用并发类的程序员会比使用wait()notify()notifyAll()方法调用直接处理同步内容的程序员更有信心。 我还将建议您自己通过同步使用这些较新的 API,但是由于种种原因,我们经常需要这样做,例如维护旧版代码。 掌握这些方法的丰富知识将在您到达这种情况时为您提供帮助。

在本教程中,我将讨论 Java 中wait() notify() notifyall()的用途。 我们将了解waitnotify之间的区别

阅读更多: Java 中的wait()sleep()之间的区别

1. 什么是wait()notify()notifyAll()方法?

Java 中的Object类具有三个最终方法,这些方法允许线程就资源的锁定状态进行通信。

  1. wait()

    它告诉调用线程放弃锁定并进入睡眠状态,直到其他线程进入同一监视器并调用notify()为止。 wait()方法在等待之前释放锁,并在从wait()方法返回之前重新获取锁。 实际上,wait()方法与同步锁紧密集成在一起,使用的特性无法直接从同步机制获得。

    换句话说,我们不可能仅在 Java 中实现wait()方法。 它是本地方法

    调用wait()方法的常规语法如下:

    synchronized( lockObject )
    { 
    	while( ! condition )
    	{ 
    		lockObject.wait();
    	}
    
    	//take the action here;
    }
    
    
  2. notify()

    它唤醒一个在同一对象上调用wait()的单个线程。 应该注意的是,调用notify()实际上并没有放弃对资源的锁定。 它告诉等待的线程该线程可以唤醒。 但是,直到通知者的同步块完成后才真正放弃锁定。

    因此,如果通知者在资源上调用notify(),但通知者仍需要在其同步块内对该资源执行 10 秒的操作,则一直在等待的线程将至少需要再等待通知者 10 秒,来释放对象上的锁定,即使调用了notify()

    调用notify()方法的常规语法如下:

    synchronized(lockObject) 
    {
    	//establish_the_condition;
    
    	lockObject.notify();
    
    	//any additional code if needed
    }
    
    
  3. 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 如果根本只执行一个线程,为什么要唤醒所有线程?

有几个原因。 例如,可能有多个条件要等待。 由于我们无法控制哪个线程获取通知,因此通知完全有可能唤醒正在等待完全不同条件的线程。

通过唤醒所有线程,我们可以设计程序,以便线程在它们之间决定下一步应执行哪个线程。 另一种选择是生产者生成的数据可以满足多个消费者的需求。 由于可能难以确定有多少消费者可以对该通知感到满意,因此可以选择全部通知他们,从而允许消费者在他们之间进行分类。

学习愉快!