3

面试官提问:线程中的wait和notify方法有啥作用?

 8 months ago
source link: https://www.51cto.com/article/769521.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

面试官提问:线程中的wait和notify方法有啥作用?

作者:了不起 2023-10-12 07:35:45
本文主要围绕线程之间的协调和通信相关技术进行一些知识总结,使用Object​类中的wait()、notify()、notifyAll()​方法,可以实现线程之间的协调和通信,但是它们只有在synchronized​修饰的同步方法/同步代码块才会生效。

在之前的线程系列文章中,我们介绍了synchronized和volatile关键字,使用它能解决线程同步的问题,但是它们无法解决线程之间协调和通信的问题。

举个简单的例子,比如线程 A 负责将 int 型变量 i 值累加操作到 10000,然后通知线程 B 负责把结果打印出来。

这个怎么实现呢?其中一个最简单的办法就是,线程 B 不断的通过轮询方式while(i == 10000)检查是否满足条件,这样就可以实现了。

虽然这种方式可以实现需求,但是也带来了另一个问题:线程 B 中的while()操作不会释放 CPU 资源,会导致 CPU 一直在这个方法上做判断操作,极大的浪费 CPU 资源。

我们知道 CPU 资源是非常非常昂贵的,因为使用 CPU 资源不只是当前一个应用程序,还有其它许许多多的应用程序。如果把这些轮询的时间释放出来,给别的线程使用,更能显著提升应用程序的运行效率。比如,线程 A 操作完成之后,通知线程 B 进行后续的操作,线程 B 无需通过轮询检查的方式来完成线程之间的协调,这样是不是更好。

在 Java 的父类中,也就是Object类中,就有三个方法:wait()、notify()、notifyAll(),它们就可以实现线程之间的通信。

如果没有接触多线程,这些方法可能基本上使用不到。下面我们一起来看看它们的使用方式!

二、方法介绍

  • wait()

wait()方法,顾名思义,表示等待的意思,它的作用是:使执行当前代码的线程进入阻塞状态,将当前线程置入"预执行队列"中,并且wait()所在的代码处停止执行,直到接到通知或被中断。

不过有个前提,在调用wait()方法之前,线程必须获得该对象的锁,因此只能在synchronized修饰的同步方法/同步代码块中调用wait()方法;同时,wait()方法执行后,会立即释放获得的对象锁以便其它线程使用,当前线程被阻塞,进入等待状态。

至于wait()为什么有阻塞的效果,其内部机制非常复杂,主要由 JVM 的 C 代码实现,大家了解就行。

  • notify()

notify()方法,顾名思义,表示通知的意思,它的作用是:让处于同一监视器下的等待线程被重新唤醒,如果有多个线程等待,那么随机挑选出一个等待的线程,对其发出通知notify(),并使它等待获取该对象的对象锁。

注意“等待获取该对象的对象锁”,这意味着即使收到了通知,等待的线程也不会马上获取对象锁,必须等待notify()方法的线程释放锁才可以。

调用环境和wait()一样,notify()也要在synchronized修饰的同步方法/同步代码块中调用。

  • notifyAll()

notifyAll()方法,顾名思义,也是表示通知的意思,它的作用是:让所有处于同一监视器下的等待线程被重新唤醒,notify()方法只会随机的唤醒一个线程,而使用notifyAll()方法将一次性全部唤醒。

通常来说,notifyAll()方法更安全,因为当我们的代码逻辑考虑不周的时候,使用notify()会导致只唤醒了一个线程,而其他线程可能永远等待下去醒不过来了。

调用环境和notify()一样,notifyAll()也要在synchronized修饰的同步方法/同步代码块中调用。

三个方法总结下来就是:

  • 1.wait()方法,使线程阻塞,进入等待状态
  • 2.notify()方法,唤醒处于等待的线程,如果有多个线程就随机从中取一个
  • 3.notifyAll()方法,唤醒所有处于等待的线程

2.1、wait/notify/notifyAll 使用介绍

通常wait()方法,一般与notify()或者notifyAll()搭配使用比较多。

下面我们看一个简单的示例。

public class MyThreadA extends Thread{

    private Object lock;

    public MyThreadA(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " wait begin");
            try {
                // 进入阻塞等待
                lock.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " wait end");
        }
    }
}
public class MyThreadB extends Thread{

    private Object lock;

    public MyThreadB(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " notify begin");
            // 唤醒其它等待线程
            lock.notify();
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " notify end");
        }
    }
}
public class MyThreadTest {

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        MyThreadA threadA = new MyThreadA(lock);
        threadA.start();

        //过3秒再启动下一个线程
        Thread.sleep(3000);

        MyThreadB threadB = new MyThreadB(lock);
        threadB.start();
    }
}

运行服务,输出结果如下:

2023-09-28 16:42:19 当前线程:Thread-0 wait begin
2023-09-28 16:42:22 当前线程:Thread-1 notify begin
2023-09-28 16:42:22 当前线程:Thread-1 notify end
2023-09-28 16:42:22 当前线程:Thread-0 wait end

从日志上可以得出,threadA线程先启动,然后进入阻塞状态,过了 3 秒之后,再启动threadB线程,运行结束之后,通知threadA线程可以获取对象锁,最后执行完毕。

整个线程之间的协调和通信,大体就是这样的。

假如我们把threadA线程数量增加到 5 个,再来看看运行效果。

public class MyThreadTest {

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        // 创建5个wait线程
        for (int i = 0; i < 5; i++) {
            MyThreadA threadA = new MyThreadA(lock);
            threadA.start();
        }

        //过3秒再启动下一个线程
        Thread.sleep(3000);

        MyThreadB threadB = new MyThreadB(lock);
        threadB.start();
    }
}

运行服务,输出结果如下:

2023-09-28 17:02:05 当前线程:Thread-0 wait begin
2023-09-28 17:02:05 当前线程:Thread-4 wait begin
2023-09-28 17:02:05 当前线程:Thread-3 wait begin
2023-09-28 17:02:05 当前线程:Thread-2 wait begin
2023-09-28 17:02:05 当前线程:Thread-1 wait begin
2023-09-28 17:02:08 当前线程:Thread-5 notify begin
2023-09-28 17:02:08 当前线程:Thread-5 notify end
2023-09-28 17:02:08 当前线程:Thread-0 wait end

从日志中,可以很清晰的看到,当多个线程处于等待状态时,调用notify()方法,只会唤醒其中一个等待的线程;同时服务无法关闭,因为剩下的 4 个线程一直处于阻塞状态。

假如我们把MyThreadB类中的lock.notify()方法改成lock.notifyAll()方法,再看看效果怎样。

public class MyThreadB extends Thread{

    private Object lock;

    public MyThreadB(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " notify begin");
            // 唤醒所有等待的线程
            lock.notifyAll();
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " notify end");
        }
    }
}

运行服务,输出结果如下:

2023-09-28 17:18:13 当前线程:Thread-0 wait begin
2023-09-28 17:18:13 当前线程:Thread-4 wait begin
2023-09-28 17:18:13 当前线程:Thread-3 wait begin
2023-09-28 17:18:13 当前线程:Thread-2 wait begin
2023-09-28 17:18:13 当前线程:Thread-1 wait begin
2023-09-28 17:18:16 当前线程:Thread-5 notify begin
2023-09-28 17:18:16 当前线程:Thread-5 notify end
2023-09-28 17:18:16 当前线程:Thread-1 wait end
2023-09-28 17:18:16 当前线程:Thread-2 wait end
2023-09-28 17:18:16 当前线程:Thread-3 wait end
2023-09-28 17:18:16 当前线程:Thread-4 wait end
2023-09-28 17:18:16 当前线程:Thread-0 wait end

从日志上可以很清晰的看到,3 秒后所有处于等待的线程都被唤醒,并且服务运行结束。

2.2、wait 释放锁介绍

在多线程的编程中,任何时候都要关注锁,因为它对当前代码执行是否安全,发挥了重要的作用。

在上面我们提到,调用wait()方法,除了让线程进入阻塞,进入等待状态以外,还会释放锁。

我们可以看一个简单的示例就知道了。

public class MyThreadA1 extends Thread{

    private Object lock;

    public MyThreadA1(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " wait begin");
            try {
                // 进入阻塞等待
                lock.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " wait end");
        }
    }
}
public class MyThreadTest1 {

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();

        // 创建两个调用wait的线程
        MyThreadA1 threadA1 = new MyThreadA1(lock);
        threadA1.start();

        MyThreadA1 threadA2 = new MyThreadA1(lock);
        threadA2.start();
    }
}

运行服务,输出结果如下:

2023-09-28 17:31:56 当前线程:Thread-0 wait begin
2023-09-28 17:31:56 当前线程:Thread-1 wait begin

从日志结果可以清晰的看出,两个线程中其中一个调用lock.wait()之后,进入了阻塞状态,同时把对象锁也释放掉了,另一个线程拿到锁并进入同步代码块内,所以看到两个线程都打印了wait begin。

在Thread类中也有一个sleep()方法可以让当前线程阻塞,但是它们之间是有区别的,sleep()方法不会让当前线程释放锁。

我们可以看一个简单的例子。

public class MyThreadA1 extends Thread{

    private Object lock;

    public MyThreadA1(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " sleep begin");
            try {
                // 进入阻塞等待
                Thread.sleep(100);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " sleep end");
        }
    }
}
public class MyThreadTest1 {

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();

        // 创建两个调用sleep的线程
        MyThreadA1 threadA1 = new MyThreadA1(lock);
        threadA1.start();

        MyThreadA1 threadA2 = new MyThreadA1(lock);
        threadA2.start();
    }
}

运行服务,输出结果如下:

2023-09-28 17:55:20 当前线程:Thread-0 sleep begin
2023-09-28 17:55:21 当前线程:Thread-0 sleep end
2023-09-28 17:55:21 当前线程:Thread-1 sleep begin
2023-09-28 17:55:21 当前线程:Thread-1 sleep end

从日志上看,线程没有交替执行,而是串性执行。

2.3、notify/notifyAll 不释放锁介绍

于此对应的还有notify()和notifyAll(), 调用notify()或者notifyAll()方法当前线程是不会释放锁的,只有当同步方法/同步代码块执行完毕,才会释放锁。

同样的,我们可以看一个简单的示例。

public class MyThreadA2 extends Thread{

    private Object lock;

    public MyThreadA2(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " notify begin");
            // 唤醒其它等待线程
            lock.notify();
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " notify end");
        }
    }
}
public class MyThreadTest2 {

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();

        // 创建两个调用notify()的线程
        MyThreadA2 threadA1 = new MyThreadA2(lock);
        threadA1.start();

        MyThreadA2 threadA2 = new MyThreadA2(lock);
        threadA2.start();
    }
}

运行服务,输出结果如下:

2023-09-28 18:11:36 当前线程:Thread-0 notify begin
2023-09-28 18:11:36 当前线程:Thread-0 notify end
2023-09-28 18:11:36 当前线程:Thread-1 notify begin
2023-09-28 18:11:36 当前线程:Thread-1 notify end

从日志结果可以清晰的看出,两个线程没有交替执行,而是串行执行。

2.4、IllegalMonitorStateException 异常介绍

虽然wait()、notify()、notifyAll()方法是在 Object 类中,理论上每个类都可以直接调用,但不是每个地方都可以随便调用,如果调用这三个方法,不在同步方法/同步代码块中,程序运行时会直接抛一次抛异常java.lang.IllegalMonitorStateException。

下面我们看一个简单的示例就知道了。

public class MyThreadTest3 {

    public static void main(String[] args) throws Exception {
        Object lock = new Object();
        lock.wait();
    }
}

运行程序,直接抛异常。

Exception in thread "main" java.lang.IllegalMonitorStateException
 at java.lang.Object.wait(Native Method)
 at java.lang.Object.wait(Object.java:502)
 at com.example.thread.e3.MyThreadTest3.main(MyThreadTest3.java:19)

换成notify()、notifyAll(),运行结果也是一样。

本文主要围绕线程之间的协调和通信相关技术进行一些知识总结,使用Object类中的wait()、notify()、notifyAll()方法,可以实现线程之间的协调和通信,但是它们只有在synchronized修饰的同步方法/同步代码块才会生效。如果不在同步方法/同步代码块调用,会抛java.lang.IllegalMonitorStateException异常。

文章内容难免有所遗漏,欢迎网友留言指出!

1、廖雪峰 - wait和notify介绍

2、五月的仓颉 - wait()和notify()/notifyAll()介绍


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK