掘金 后端 ( ) • 2024-06-23 18:14

highlight: gruvbox-dark theme: cyanosis

你好,我是 shengjk1,多年大厂经验,努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注!你会有如下收益:

  1. 了解大厂经验
  2. 拥有和大厂相匹配的技术等

希望看什么,评论或者私信告诉我!

一、前言

在软件开发中,线程是实现并发执行的重要手段,然而,线程之间的协作与通信却是开发者必须重点考虑的挑战之一。Java作为一种广泛应用于多线程编程的语言,提供了一套强大而灵活的机制,让不同线程之间能够优雅地交替执行、传递信息,以实现协调合作。

前面我们已经完成了多线程一些基础知识准备以及 volatile 和 synchronized,本文将深入探讨Java中通过notifywait实现线程间通信的机制。

二、notify 和 wait

2.1 wait

2.1.1 wait 基本介绍

通过源码我们可以知道 wait 是 object 对象方法,用于实现线程间的等待和通知机制。当一个线程调用wait()方法时,它会释放当前所持有的对象锁,并进入等待状态,直到被其他线程调用相同对象上的notify()notifyAll()方法唤醒。

public final void wait() throws InterruptedException {
    wait(0);
}

2.1.2 wait 注意点

  1. 要想使用wait方法,当前线程必须拥有对应 object 的 mointor,即必须要拥有对应对象的锁,否则会报java.lang.IllegalMonitorStateException 比如:
Object o = new Object();
o.wait(); //java.lang.IllegalMonitorStateException
  1. 当调用wait()方法的线程被另一个线程中断时,wait()方法会抛出InterruptedException异常。
  2. 线程也可能会在没有收到通知、中断或超时的情况下被唤醒,即所谓的虚假唤醒。虽然这种情况在实践中很少发生,但应用程序必须通过测试应该导致线程被唤醒的条件来防范这种情况,如果条件不满足,则继续等待。换句话说,等待应该总是在循环中发生,如下所示:
   synchronized (obj) {
          while (<condition does not hold>)
              obj.wait(timeout);
          ... // Perform action appropriate to condition
      }

2.1.3 wait 使用场景

wait()方法是在Java中用于线程间通信和同步的重要方法之一。它通常与synchronized关键字一起使用,用于在多线程中协调线程之间的操作。下面是wait()方法的一些常见使用场景:

  1. 协调多个线程的操作wait()方法通常与notify()notifyAll()方法一起使用,用于在多线程之间协调操作。一个线程可以调用wait()进入等待状态,等待其他线程调用相同对象的notify()notifyAll()方法来唤醒它。
  2. 等待条件满足:线程可以调用wait()方法等待某个条件的满足。例如,一个线程可能等待某个变量的值发生改变或者等待某个事件发生。
  3. 线程安全的队列实现wait()方法常用于实现线程安全的队列。当队列为空时,消费者线程调用wait()等待生产者线程向队列中添加元素;当队列已满时,生产者线程调用wait()等待消费者线程从队列中取走元素。
  4. 实现生产者-消费者模型wait()方法在生产者-消费者模型中起着重要作用。生产者向共享的缓冲区添加数据时,如果缓冲区已满,生产者线程调用wait()等待消费者取走数据;消费者从缓冲区取数据时,如果缓冲区为空,消费者线程调用wait()等待生产者添加数据。
  5. 线程间通信wait()方法是线程间通信的重要手段之一。线程可以通过等待并唤醒的机制来实现信息的传递和协调。

2.1.4 wait 的执行原理

  • 当线程调用wait()方法时,它会释放当前持有的对象锁,使得其他线程可以访问这个对象并执行同步代码块。
  • 调用wait()方法的线程会进入对象的等待队列,等待其他线程调用相同对象的notify()notifyAll()方法来唤醒它。
  • 当另一个线程调用相同对象notify()方法或notifyAll()方法时,等待队列中的线程会被唤醒,然后竞争对象的锁。
  • 唤醒的线程会尝试重新获取对象锁,然后继续执行。

2.1.5 wait 使用

  1. wait()或者wait(0): 调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意调用 wait() 方法后,会释放对象的锁
  2. wait(long) 会释放锁,超时等待一段时间,这里的参数时间是毫秒,也就是等待长达毫秒,如果没有通知就超时返回
  3. wait(long,int) 会释放锁,该方法与 wait(long) 类似,多了一个 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999), 所以超时的时间还需要加上 nanos 纳秒。

2.2 notify

首先我们需要知道 nofity 和 notifyall 基本上是等价的,如没有特别标明,nofity 和 nofityall 是一样的

2.2.1 notify 基本介绍

通过源码我们可以知道 notify 是 object 对象方法。

notify()是线程间通信的一种机制,用于唤醒在当前对象上等待的一个线程。当一个线程调用notify()方法时,它会唤醒正在该对象上等待的单个线程(如果有多个线程在等待,系统无法确定哪个线程会被唤醒,因为选择是随机的)。这个被唤醒的线程将从等待状态变为可运行状态,但并不意味着立即获得对象的锁。

public final native void notify();

当一个线程调用notifyAll()时,它会唤醒在当前对象上等待的所有线程,使它们从等待状态转变为可运行状态。这样,所有等待中的线程都有机会争取获取对象锁,在某个线程获得锁后,它们会竞争执行。

2.2.2 notify 注意点

下面是关于notify()方法的一些重要点:

  1. 使用条件变量notify()方法通常与条件变量一起使用,用于线程间的协作。一个线程等待某个条件变为真,另一个线程在某种情况下会改变条件,并调用notify()来通知等待的线程。
  2. notifyAll()方法:与notify()不同的是,notifyAll()方法唤醒在当前对象上等待的所有线程,而不仅仅是一个线程。这样做可以避免遗漏任何等待中的线程,但同一时间获取锁的只会有一个线程。
  3. Object类中的方法notify()方法是Object类的一个方法,因此任何Java对象都可以调用notify()wait()方法来进行线程间的通信。
  4. 必须在同步块中调用:为了调用notify()方法,必须在同步块(synchronized块)中对包含该对象的锁进行操作。
  5. 随机性:当多个线程在同一个对象上等待时,调用notify()会随机选择一个线程唤醒,因此不能确定哪个线程会被唤醒。
  6. 唤醒等待线程:被唤醒的线程会尝试重新获取对象锁,一旦获得锁,它会从wait()方法返回,并继续执行后续代码。

2.2.3 nofityall

以下是关于notifyAll()方法的详细介绍:

  1. 唤醒所有等待线程notifyAll()方法被调用时,会唤醒在当前对象上等待的所有线程,使它们从阻塞状态转变为就绪状态。这样,所有等待中的线程都有机会去竞争获取对象的锁。
  2. 解决等待问题notifyAll()通常用于解决多线程之间的通信和协调问题。当某个条件得到满足时,调用notifyAll()可以通知所有等待该条件的线程。
  3. 谨慎使用:需要谨慎使用notifyAll(),因为唤醒所有线程可能会导致竞争和性能问题。在某些情况下,如果只有一个线程是合适的接收方,那么使用notify()来唤醒特定线程会更有效率。
  4. 必须在同步块中调用:就像notify()方法一样,notifyAll()也必须在同步块(synchronized块)内调用,以确保在对象上同步。通过同步块,确保在调用notifyAll()时,持有对象的锁。
  5. 竞争获取锁:被唤醒的线程会尝试获取对象的锁,一旦获得锁,它们将从wait()方法返回,并开始竞争执行。
  6. 释放锁:调用notifyAll()并不会释放对象锁,它仅唤醒等待的线程,这些线程会在合适的时机争夺锁。

2.2.3 notify 使用场景

notify()的使用场景:

  • 单个唤醒:当只需唤醒等待队列中的一个线程时,适合使用notify()。这种情况下,最多只有一个等待线程被唤醒,选择被唤醒的线程是不确定的。
  • 资源更新:当某个共享资源的状态发生变化时,并且只需要通知一个线程来处理这种变化时,可以使用notify()

notifyAll()的使用场景:

  • 多个唤醒:当需要唤醒所有等待线程来处理某个共享资源的变化时,应该使用notifyAll()。这种情况下,所有等待线程都会被唤醒。
  • 条件变更:当共享资源的状态发生变化对多个线程都有影响时,可以使用notifyAll()来通知所有等待线程,因为多个线程可能对资源的状态变化做出不同的响应。
  • 避免竞争问题:在某些情况下,使用notifyAll()可以避免因为只唤醒一个线程而导致的竞争问题,确保所有等待线程都能及时做出响应。

2.2.4 notify 的执行原理

notify()方法的执行原理:

  1. 当一个线程调用对象的notify()方法时,这个对象的监视器被激活。
  2. 在等待该对象的线程中,会有一个线程从等待队列中被选中,进入到锁定状态,并尝试重新获取对象的锁。
  3. 一旦获取到锁,该线程会从wait()方法返回,并继续执行。
  4. 其他等待该对象的线程仍然处于等待状态,需要重新竞争获取对象的锁来继续执行。

notifyAll()方法的执行原理:

  1. 当一个线程调用对象的notifyAll()方法时,所有等待该对象的线程将被唤醒。
  2. 被唤醒的线程会一起竞争对象的锁,以便能够继续执行。
  3. 每个被唤醒的线程尝试重新获取锁,一旦成功获取到锁,它们会从wait()方法返回,并接着执行后续代码。

2.2.5 notify和notifyAll 注意事项和要点:

  • 使用 notify和notifyAll 时需要先对调用的对象加锁,否则会报错
  • notify()方法和notifyAll()方法必须在同步块中(synchronized块)调用,以确保在调用这些方法时对象的锁处于正确状态。
  • 被唤醒的线程在获取到锁并从wait()方法返回后,可能需要检查等待期间的条件是否发生了变化,从而决定是否继续执行。

三、wait/nofity 经典使用方式

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

public class WaitNotify {
    static boolean flag = true;
    static Object  lock = new Object();

    public static void main(String[] args) {
       Thread waitThread = new Thread(new Wait(), "waitThread");
       waitThread.start();
       try {
          TimeUnit.SECONDS.sleep(1);
       } catch (InterruptedException e) {
          e.printStackTrace();
       }

       new Thread(new Notify(), "notifyThread").start();


    }

//wait
    static class Wait implements Runnable {
       public void run() {
          synchronized (lock) {
             while (flag) {
                try {
                   System.out.println(Thread.currentThread() + " flag is true" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                   //没有释放资源 wait 等待
                   lock.wait();
                } catch (InterruptedException e) {
                }
             }
             System.out.println(Thread.currentThread() + " flag is false" + new SimpleDateFormat("HH:mm:ss").format(new Date()));

          }
       }
    }

    static class Notify implements Runnable {
       public void run() {
          synchronized (lock) {
             while (flag) {
                System.out.println(Thread.currentThread() + "hold lock  notify" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                //资源准备
                好了,nofity
                lock.notifyAll();
                flag = false;
                try {
                   TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                   e.printStackTrace();
                }

             }
          }
          synchronized (lock) {
             System.out.println(Thread.currentThread() + "hold lock again. notify" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
             try {
                TimeUnit.SECONDS.sleep(5);
             } catch (InterruptedException e) {
                e.printStackTrace();
             }

          }
       }
    }
}

这是最经典的等待/通知的范式:但资源没有准备好时,wait 等待,当资源准备好了,notify 通知。这里也留一个思考题:这里的 flag 为什么不需要用 volatile 修饰,答案在这里:多线程一些基础知识准备以及 volatile 和 synchronized

四、总结

文章详细讲解了Java中waitnotify方法的使用方法和注意事项,通过这些方法实现线程间的协调与通信,并列举了常见的使用场景,如协调多个线程操作、实现生产者-消费者模型等。此外,文章还强调了waitnotify方法必须在同步块中调用,以确保线程安全。