Bootstrap

多线程编程中的“等待和通知机制”:wait 和 notify 方法

目录

一、等待和通知机制的概念

二、wait() 方法

2.1 wait() 方法的使用

2.2 超时等待

2.3 异常唤醒

2.4 唤醒等待的方法

三、notify() 方法

四、notifyAll() 方法

五、wait 和 sleep 的对比


一、等待和通知机制的概念

1)什么是等待和通知机制?

线程是抢占式执行的,无法预知线程之间的执行顺序。

但有时程序员也希望能合理协调多个线程的执行顺序。

因此,在 Java 中使用了等待(wait)和通知(notify)机制,用于在应用层面上干预多个线程的执行顺序

应当注意的是,干预执行顺序并不是干预系统的线程调度策略(操作系统内核中的线程调度仍是无序的),而是使被指定的线程,主动放弃被系统调度的机会,直到其他线程对被指定的线程发出通知,这个线程才再次参与系统的线程调度。(排队都不排了,自然也就轮不到它了)

2)使用等待和通知机制主要涉及以下三个方法
wait()让线程进入等待状态。
notify()唤醒在当前对象上等待的一个线程。
notifyAll()唤醒在当前对象上等待的所有线程。
以上三个方法都是 Object 类的方法。

二、wait() 方法

2.1 wait() 方法的使用

1)wait() 方法需要配合 synchronized 关键字使用

wait() 方法必须在 synchronized 修饰的代码块或方法中使用,否则会抛出 IllegalMonitorStateException 异常。

2)使用锁对象调用 wait() 方法

虽然,wait() 是 Object 类的方法,任何对象都可以调用该方法。

但是为了实现等待通知机制,要求调用 wait() 的对象必须是锁对象,且这个锁对象要与 synchronized 指定的锁对象一致。

3)wait() 方法具体做了什么?

wait() 方法主要执行了以下三个操作:

<1>释放当前的锁。
<2>使当前线程进入等待队列。
<3>通过某些条件被唤醒时,重新尝试获取当前锁。

代码演示wait()使用方法和使用结果:

    public static void main(String[] args) throws InterruptedException {
        //创建锁对象;
        Object locker = new Object();
        
        System.out.println("wait前");
        //在 synchronized 代码块中调用 wait 方法;
        synchronized (locker){
            // wait 方法是由锁对象调用的,调用后,线程释放锁,进入等待队列;
            locker.wait();
        }
        System.out.println("wait后");
    }

//运行结果:
wait前
...

程序没有执行完毕,线程一直在 wait 。

2.2 超时等待

有时间限制的等待

上文中的代码,除非有其他线程唤醒,否则执行后会一直处于 wait 的状态,这就使得程序陷入了“停摆”状态。

在部分场景中,我们可以使用带时间参数的 wait() 方法来规避这个问题。即使没有其他线程唤醒 wait ,wait 仍会在超过规定时间后,自动唤醒,避免了程序的“停摆”。

代码演示带时间参数的wait()使用方法和使用结果:

    public static void main(String[] args) throws InterruptedException {
        //创建锁对象;
        Object locker = new Object();

        System.out.println("wait前");
        //在 synchronized 代码块中调用 wait 方法;
        synchronized (locker){
            // wait 方法是由锁对象调用的,调用后,线程释放锁,进入等待队列;
            locker.wait(3000);
            //3秒后线程被唤醒;
        }
        System.out.println("wait后");
    }

//运行结果:
wait前
wait后

成功执行完毕。

2.3 异常唤醒

代码演示通过抛出异常唤醒wait:

    public static void main(String[] args) {
        //创建锁对象;
        Object locker = new Object();

        Thread t1 = new Thread(()->{
            System.out.println("wait前");
            //在 synchronized 代码块中调用 wait 方法;
            synchronized (locker){
                // wait 方法是由锁对象调用的,调用后,线程释放锁,进入等待队列;
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //3秒后线程被唤醒;
            }
            System.out.println("wait后");
        });

        t1.start();

        //抛出异常,清除中断标志,唤醒t1线程。
        t1.interrupt();
    }

//运行结果:
wait前
wait后
java.lang.InterruptedException

抛出了InterruptedException后,线程被唤醒,继续执行。

2.4 唤醒等待的方法

唤醒等待的方法有三种:
<1>

超时等待:超过了 wait() 方法指定的等待时间。

<2>异常唤醒:通过其他线程调用该等待线程的 interrupted() 方法,抛出异常唤醒。
<3>notify() 方法:其他线程调用该对象的 notify() 方法。

三、notify() 方法

1)notify() 方法有什么作用?

notify() 方法可以唤醒等待的线程。

2)notify() 也要写在 synchronized 修饰的代码块或方法中

notify() 方法也是 Object 类的方法,所以任何对象都可以调用。

在操作系统的原生 API 中,也有 wait() 和 notify() 方法。与 wait() 方法不同,操作系统的原生 API 没有要求 notify() 必须在 synchronized 修饰的代码块或方法中使用。

但是,应注意,在 Java 中还是特别约定了 notify() 方法也是要放在 synchronized 修饰的代码块或方法中的。

3)wait() 和 notify() 方法是通过锁对象联系的

一个锁对象调用的 wait() 只能被同一个锁对象调用的 notify() 唤醒。

如果唤醒时,同一个锁对象有多个线程正在等待,此时只会随机唤醒一个。

4)执行 notify() 方法后,锁在什么时候释放?

在 notify() 方法后,当前线程不会马上释放锁对象,而是等到线程执行完 notify() 方法所在的代码块或方法后,才会释放锁对象。

这也是 Java 中约定 notify() 方法要放在 synchronized 修饰的代码块或方法中的原因。

代码演示通过 notify 唤醒wait:

    public static void main(String[] args) throws InterruptedException {
        //创建一个锁对象;
        Object locker = new Object();

        //创建一个线程;
        Thread t1 = new Thread(()->{
            System.out.println("wait前");
            //打印"wait前"后等待;
            synchronized (locker){
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            //被唤醒后打印"wait后";
            System.out.println("wait后");
        });
        t1.start();

        //休眠两秒,保证t1线程进入等待状态。
        Thread.sleep(2000);
        //打印"notify前"后唤醒;
        System.out.println("notify前");
        synchronized (locker){
            locker.notify();
            //在出代码块前,打印"notify后"。
            System.out.println("notify后");
        }
    }

//运行结果:
wait前
notify前
notify后
wait后

打印"wait前"之后进入阻塞等待,直到被notify唤醒之后才打印了"wait后"。

四、notifyAll() 方法

notifyAll() 方法有什么作用?

唤醒这个锁对象上所有等待的线程。

有多个线程使用同一个锁对象 wait ,当对这个锁对象使用 notifyALL() 方法时,所有在等待的线程都会唤醒。

但是需要注意,在唤醒之后,由于需要重新获取锁,此时被唤醒的线程必然要进行锁竞争,所以这些被唤醒的线程并不是同时就开始执行各自的代码了,而仍然是有先后顺序的执行,顺序依旧是随机的。


五、wait 和 sleep 的对比

相同点
都会使线程阻塞等待。
不同点wait() 方法sleep() 方法
用途用于线程间通信。用于线程阻塞等待。
用法

是 Object 类中的方法,

需要在被 synchronized 修饰的代码块或方法中使用。

是 Tread 类中的静态方法,

方法的使用与 synchronized 无关。

状态

被调用后,当前线程进入 BLOCK 状态并释放锁。

被调用后,当前线程进入 TIME_WAIT 状态。
唤醒

通常通过 notify 唤醒;

可以通过超时或抛出异常唤醒;

通常按设定的时间唤醒;

可以通过抛出异常唤醒;


阅读指针 -> 《经典设计模式之 -- 单例模式(“饿汉模式”和“懒汉模式”实现单例模式)》

单例模式的两种实现:“饿汉模式”和“懒汉模式”介绍了什么是单例模式和单例模式的两种实现模式。重点介绍了单例模式中,“懒汉模式”在多线程下的实现。https://blog.csdn.net/zzy734437202/article/details/134785459

;