wait(),notify(),notifyAll()_多线程wait和sleep

wait(),notify(),notifyAll()_多线程wait和sleep前言在上一篇中,我们介绍了Java中的线程的基本概念,我们了解到线程是有很多种状态的,本章,我们就来聊聊线程中的状态是如何进行控制与切换的。Java中提供了很多种方法对线程的状态进行控制以及线程之间的通信,包括wait、notify、notifyAll、sleep,下面我们就来看一下它们之间有什么区别,以及如何使用这些方法进行线程状态的控制与通信。线程之间的通信在Java中可以用w…

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

前言

在上一篇中,我们介绍了Java中的线程的基本概念,我们了解到线程是有很多种状态的,本章,我们就来聊聊线程中的状态是如何进行控制与切换的。Java中提供了很多种方法对线程的状态进行控制以及线程之间的通信,包括wait、notify、notifyAll、sleep,下面我们就来看一下它们之间有什么区别,以及如何使用这些方法进行线程状态的控制与通信。

线程之间的通信

在Java中可以用wait、notify和notifyAll来实现线程间的通信。举个例子,如果你的Java程序中有两个线程——即生产者和消费者,那么生产者可以通知消费者,让消费者开始消耗数据,因为队列缓冲区中有内容待消费(不为空)。相应的,消费者可以通知生产者可以开始生成更多的数据,因为当它消耗掉某些数据后缓冲区不再为满。

wait与notify

在Object对象中有三个方法wait()、notify()、notifyAll(),它们的用途都是用来控制线程的状态。

  1. wait()

该方法用来将当前线程置入休眠状态,直到在其他线程调用此对象的notify()方法或notifyAll()方法将其唤醒。

在调用wait()之前,线程必须要获得该对象的对象级别锁,因此只能在同步方法或同步块中调用wait()方法。进入wait()方法后,当前线程释放锁。在从wait()返回前,线程与其他线程竞争重新获得锁。如果调用wait()时,没有持有适当的锁,则抛出IllegalMonitorStateException,它是RuntimeException的一个子类,因此,不需要try-catch结构。

  1. notify()

该方法唤醒在此对象监视器上等待的单个线程。如果有多个线程都在此对象上等待,则会随机选择唤醒其中一个线程,对其发出通知notify(),并使它等待获取该对象的对象锁。注意“等待获取该对象的对象锁”,这意味着,即使收到了通知,wait的线程也不会马上获取对象锁,必须等待notify()方法的线程释放锁才可以。和wait()一样,notify()也要在同步方法/同步代码块中调用。

总结两个方法:wait()使线程停止运行,notify()使停止运行的线程继续运行。

说了一大堆概念,可能有点绕,下面我们看两个例子来理解一下这两个方法的具体使用方式。

ThreadNotify类:

public class ThreadNotify {

    private Object lock;

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

    public void testNotify() {
        try {
            synchronized (lock) {
                System.out.println("start notify........");
                lock.notify();
                System.out.println("end notify........");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

ThreadWait类:

public class ThreadWait {

    private Object lock;

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

    public void testWait() {
        try {
            synchronized (lock) {
                System.out.println("start wait........");
                lock.wait();
                System.out.println("end wait........");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public class ThreadWaitNotifyDemo { 
   
    public static void main(String[] args) throws Exception {
        Object lock = new Object();
        Thread waitThread = new Thread(() -> {
            ThreadWait threadWait = new ThreadWait(lock);
            threadWait.testWait();
        });
        Thread notifyThread = new Thread(() -> {
            ThreadNotify threadNotify = new ThreadNotify(lock);
            threadNotify.testNotify();
        });
        waitThread.start();
        /** * 保证waitThread一定会先开始启动 */
        Thread.sleep(1000);
        notifyThread.start();
    }
}

执行结果:

start wait........
start notify........
end notify........
end wait........

在上面的例子中,我们创建了两个类,分别是ThreadWait、ThreadNotify,ThreadWait负责让线程进行等待操作,ThreadNotify负责唤醒线程的操作,从上面的例子中,我们可以得知几点信息:

  • wait()方法可以使调用该线程的方法释放持有当前对象的锁,然后从运行状态退出,进入等待队列,直到再次被唤醒。
  • notify()方法可以随机唤醒等待队列中等待的一个线程,并使得该线程退出等待状态,进入可运行状态

上面的例子中,wait方法与notify方法全部在同步代码块中进行的执行,如果不这样会出现什么样子的效果呢?我们来试一下。我们将同步代码块去掉,再次执行:

start wait........
java.lang.IllegalMonitorStateException
    at java.lang.Object.wait(Native Method)
    at java.lang.Object.wait(Object.java:502)
    at com.xuangy.concurrency.practice.ThreadWait.testWait(ThreadWaitNotifyDemo.java:37)
    at com.xuangy.concurrency.practice.ThreadWaitNotifyDemo.lambda$main$0(ThreadWaitNotifyDemo.java:11)
    at java.lang.Thread.run(Thread.java:745)
start notify........
java.lang.IllegalMonitorStateException
    at java.lang.Object.notify(Native Method)
    at com.xuangy.concurrency.practice.ThreadNotify.testNotify(ThreadWaitNotifyDemo.java:56)
    at com.xuangy.concurrency.practice.ThreadWaitNotifyDemo.lambda$main$1(ThreadWaitNotifyDemo.java:15)
    at java.lang.Thread.run(Thread.java:745)

Process finished with exit code 0

发现两个线程中均抛出了异常,说明如果wait()方法和notify()方法不在同步方法/同步代码块中被调用,那么虚拟机会抛出java.lang.IllegalMonitorStateException。

而抛出的IllegalMonitorStateException异常又是什么?我们可以看一下JDK中对IllegalMonitorStateException的描述:

Thrown to indicate that a thread has attempted to wait on an object's 
monitor or to notify other threads waiting on an object's monitor 
without owning the specified monitor.

这句话的意思大概就是:线程试图等待对象的监视器或者试图通知其他正在等待对象监视器的线程,但本身没有对应的监视器的所有权。

wait方法是一个本地方法,其底层是通过一个叫做监视器锁的对象来完成的。所以上面之所以会抛出异常,是因为在调用wait方式时没有获取到monitor对象的所有权。

因此在执行wait与notify方法之前,必须拿到被调用对象的对象锁,才可以进行等待或唤醒操作。

wait()与notify()操作会释放锁吗?

在上面的例子中,我们多次强调了锁的问题,那么执行wait()方法后或notify()后,会释放掉持有对象的对象锁吗?我们通过下面的例子来实验一下:

public class ThreadWaitNotifyLockDemo { 
   
    public static void main(String[] args) throws Exception {
        Object lock = new Object();
        Thread waitThread1 = new Thread(() -> { ThreadWait threadWait = new ThreadWait(lock); threadWait.testWait(); }); Thread waitThread2 = new Thread(() -> { ThreadWait threadWait = new ThreadWait(lock); threadWait.testWait(); }); waitThread1.start(); waitThread2.start(); } }

执行结果:

start wait........
start wait........

在上面的例子中,我们启动两个线程,都去执行线程等待的操作,从执行结果看到,输出了两条“start wait”,这个可以说明,wait()操作会释放掉当前持有对象的锁,否则第二个线程根本不会进入代码块中执行。

OK,我们通过上面的例子得到结论,wait()操作会释放其持有的对象锁,那么notify()操作是否也是一样的呢?我们再通过一个例子来实验一下:

ThreadNotify类:

public class ThreadNotify {

    private Object lock;

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

    public void testNotify() {
        try {
            synchronized (lock) {
                System.out.println("start notify........" + Thread.currentThread().getName());
                lock.notify();
                //线程休息两秒
                Thread.sleep(2000);
                System.out.println("end notify........" + Thread.currentThread().getName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public class ThreadWaitNotifyLock2Demo { 
   
    public static void main(String[] args) throws Exception {
        Object lock = new Object();
        Thread waitThread1 = new Thread(() -> { ThreadWait threadWait = new ThreadWait(lock); threadWait.testWait(); }); Thread notifyThread1 = new Thread(() -> { ThreadNotify threadNotify = new ThreadNotify(lock); threadNotify.testNotify(); }); Thread notifyThread2 = new Thread(() -> { ThreadNotify threadNotify = new ThreadNotify(lock); threadNotify.testNotify(); }); Thread notifyThread3 = new Thread(() -> { ThreadNotify threadNotify = new ThreadNotify(lock); threadNotify.testNotify(); }); waitThread1.start(); notifyThread1.start(); notifyThread2.start(); notifyThread3.start(); } }

执行结果:

start wait........
start notify........Thread-3
end notify........Thread-3
start notify........Thread-2
end notify........Thread-2
start notify........Thread-1
end notify........Thread-1
end wait........

这个例子中,我们启动了四个线程,第一个线程执行等待操作,其他两个线程执行唤醒操作,从执行结果中可以看到,当第一次notify后,线程休息了2秒,如果notify释放锁,那么在其sleep的时候,必然会有其他线程争抢到锁并执行,但是从结果中,可以看到这并没有发生,由此可以说明notify()操作不会释放其持有的对象锁。

永远在循环(loop)里调用 wait 和 notify,不是在 If 语句

通过上面的几个例子,我们现在已经知道了wait()应该永远在被synchronized同步代码块或同步方法中进行调用,而需要着重注意的一点是:应该永远在while循环,而不是if语句中调用wait。

为线程是在某些条件下等待的————我们在实际开发中往往会设定一些条件,而使线程进入等待,这个时候你可能直觉就会写一个if语句。但if语句存在一些微妙的小问题,导致即使条件没被满足,你的线程你也有可能被错误地唤醒。所以如果你不在线程被唤醒后再次使用while循环检查唤醒条件是否被满足,你的程序就有可能会出错。

所以,根据JDK给出的代码示例,应该这样去使用wait():

synchronized (obj) {
    while (<condition does not hold>)
        obj.wait();
        // Perform action appropriate to condition
        // Do something......
}

在while循环里使用wait的目的,是在线程被唤醒的前后都持续检查条件是否被满足。如果条件并未改变,wait被调用之前notify的唤醒通知就来了,那么这个线程并不能保证被唤醒,有可能会导致死锁问题。

interrupt

interrupt()来自于Thread类,用途是中断线程。如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException。

我们来看一下在执行wait()后进行interrupt的效果:

public class ThreadWaitInterruptDemo {
    public static void main(String[] args) {
        Object lock = new Object();
        Thread waitThread = new Thread(() -> {
            ThreadWait threadWait = new ThreadWait(lock);
            threadWait.testWait();
        });
        waitThread.start();
        waitThread.interrupt();
    }
}

执行结果:

start wait........
java.lang.InterruptedException
    at java.lang.Object.wait(Native Method)
    at java.lang.Object.wait(Object.java:502)
    at com.xuangy.concurrency.practice.ThreadWait.testWait(ThreadWait.java:18)
    at com.xuangy.concurrency.practice.ThreadWaitInterruptDemo.lambda$main$0(ThreadWaitInterruptDemo.java:11)
    at java.lang.Thread.run(Thread.java:745)

notifyAll

notifyAll也是来自于Object类的方法,其作用是唤醒在此对象监视器上等待的所有线程。其用法与notify()基本一致,只不过它会唤醒一个对象监视器上等待的全部线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。

我们来看一下它的使用方式:

public class ThreadNotifyAllDemo { 
   
    public static void main(String[] args) throws Exception {
        Object lock = new Object();
        Thread waitThread1 = new Thread(() -> { ThreadWait threadWait = new ThreadWait(lock); threadWait.testWait(); }); Thread waitThread2 = new Thread(() -> { ThreadWait threadWait = new ThreadWait(lock); threadWait.testWait(); }); Thread waitThread3 = new Thread(() -> { ThreadWait threadWait = new ThreadWait(lock); threadWait.testWait(); }); Thread waitThread4 = new Thread(() -> { ThreadWait threadWait = new ThreadWait(lock); threadWait.testWait(); }); Thread waitThread5 = new Thread(() -> { ThreadWait threadWait = new ThreadWait(lock); threadWait.testWait(); }); waitThread1.start(); waitThread2.start(); waitThread3.start(); waitThread4.start(); waitThread5.start(); Thread.sleep(2000); synchronized (lock) { lock.notifyAll(); } } }

执行结果:

start wait........Thread-1
start wait........Thread-2
start wait........Thread-3
start wait........Thread-0
start wait........Thread-4
end wait........Thread-4
end wait........Thread-0
end wait........Thread-3
end wait........Thread-2
end wait........Thread-1

从执行结果可以看到,notifyAll是不会保证唤醒的顺序的,是虚拟机随机指定的。

需要注意的是,在执行notifyAll之前,同样需要获取到对象的锁,即必须在同步方法或者同步代码块中执行,否则会抛出IllegalMonitorStateException异常。

sleep

sleep方法的作用是让当前线程暂停指定的时间(毫秒),sleep方法是最简单的方法,在上述的例子中也用到过,比较容易理解。唯一需要注意的是其与wait方法的区别。

最简单的区别是,wait方法依赖于同步,而sleep方法可以直接调用。而更深层次的区别在于sleep方法只是暂时让出CPU的执行权,并不释放锁。而wait方法则需要释放锁。

来看一个简单的例子:

public class ThreadSleepDemo { 
   
    public static void main(String[] args) {
        Thread sleepThread1 = new Thread(() -> { System.out.println("当前线程是:" + Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (Exception e) { e.printStackTrace(); } System.out.println("休息两秒结束……"); }); Thread sleepThread2 = new Thread(() -> { System.out.println("当前线程是:" + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } System.out.println("休息一秒结束……"); }); sleepThread1.start(); sleepThread2.start(); } }

执行结果:

当前线程是:Thread-0
当前线程是:Thread-1
休息一秒结束……
休息两秒结束……

结语

本篇中我们学习了wait、sleep、notify、notifyAll的使用方法和机制,对于Object类中的每一个方法,都是非常重要和精妙的,因此想使用好wait与notify、notifyAll我们需要深入的理解其机制,才能真正的使用好这些方法。

由于个人水平非常有限,对这几个方法的使用也是非常的粗浅,示例中如果有不合理的地方,希望读者多多指正,下一篇中,我们将介绍一下Java中用来实现线程同步的关键字synchronized,敬请期待!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/195999.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • griddata三维空间插值「建议收藏」

    griddata三维空间插值「建议收藏」利用griddata进行三维空间插值

    2022年5月26日
    39
  • .deb版本cuda安装。

    .deb版本cuda安装。1.先记录下驱动问题:1).run形式安装cuda。清理原有显卡驱动后,先安装自己显卡对应的驱动,在步骤中出现”Wouldyouliketorunthenvidia-xconfigutilitytoautomaticallyupdateyourXconfigurationfile…”时,选择No。(这里是cuda自带的旧版本的驱动)。我安装cuda8.0时,是…

    2022年6月1日
    61
  • 计算机的存储容量一般用什么来表示_计算机常用的存储容量单位

    计算机的存储容量一般用什么来表示_计算机常用的存储容量单位存储容量是指存储器可以容纳的二进制信息量,用存储器中存储地址寄存器MAR的编址数与存储字位数的乘积表示。中文名存储容量所属学科计算机科学与技术存储容量单位简介语音网络上的所有信息都是以“位”(bit)为单位传递的,一个位就代表一个0或1。每8个位(bit)组成一个字节(byte)。字节是什么概念呢?一个英文字母就占用一个字节,也就是8位,一个汉字占用两个字节。一般位简写为小写字母“b”,字节简写为…

    2022年10月6日
    3
  • SpringBoot集成Redis并实现主从架构「建议收藏」

    SpringBoot集成Redis并实现主从架构「建议收藏」hello,你好呀,我是灰小猿,一个超会写bug的程序猿!今天这篇文章来和大家分享一下在springboot中如何集成redis,并实现主从架构,进行数据的简单存储。我的Redis是部署在Windows系统下面的,所以在这里附上Redis在Windows环境下的安装地址和安装说明。一、Windows环境下安装Redis首先去官网下载Redis的安装包,官方下载地址:https://github.com/tporadowski/redis/releases在其中选择当前版本即可。下载之后解压

    2025年12月6日
    5
  • Java 构造函数特点「建议收藏」

    Java 构造函数特点「建议收藏」(1).一般函数是用于定义对象应该具备的功能。而构造函数定义的是,对象在调用功能之前,在建立时,应该具备的一些内容。也就是对象的初始化内容。(2).构造函数是在对象建立时由jvm调用,给对象初始化。一般函数是对象建立后,当对象调用该功能时才会执行。(3).普通函数可以使用对象多次调用,构造函数就在创建对象时调用。(4).构造函数的函数名要与类名一样,而普通的函数只要符合标识符的命名…

    2022年6月17日
    20
  • 【2022最新Java面试宝典】—— SpringBoot面试题(44道含答案)

    【2022最新Java面试宝典】—— SpringBoot面试题(44道含答案)目录1.什么是SpringBoot?2.为什么要用SpringBoot3.SpringBoot与SpringCloud区别4.SpringBoot有哪些优点?5.SpringBoot的核心注解是哪个?它主要由哪几个注解组成的?6.SpringBoot支持哪些日志框架?推荐和默认的日志框架是哪个?7.SpringBootStarter的工作原理8.SpringBoot2.X有什么新特性?与1.X有什么区别?9.SpringBoot支持什么前端模板,10.Spr

    2022年7月15日
    47

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号