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

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


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)
全栈程序员-站长的头像全栈程序员-站长

相关推荐

  • ArcGIS二次开发及应用学习的最佳网站

    ArcGIS二次开发及应用学习的最佳网站http://i.youku.com/gisxiaotian里面有大量的ArcGIS二次开发及应用的视频教程,作者会定期的上传ArcGIS教程,看了绝对受益匪浅!

    2022年7月23日
  • VS2017 安装 Microsoft Visual Studio Installer Project

    VS2017 安装 Microsoft Visual Studio Installer Project1.安装打包插件:MicrosoftVisualStudio2017安装程序项目2.联机查找下面的组件,然后安装,重启VS,进行插件安装3.新建安装项目,另外,有些人可能会想这么多安装类型,怎么选。因为我们这里说的Windows程序打包,输出文件格式为.exe或者.msi,大概看一下介绍就能知道,第一个和第四个是合适的。这两个又该怎么选呢?那其实第四个就是对一些属性多了些引导,最…

    2022年7月20日
  • 信息学奥赛GoC编程测试题题库

    信息学奥赛GoC编程测试题题库在线答题和答案详解请参考https://blog.csdn.net/zhengzyx2040/article/details/118632561一、单选题(15题,每题4分,满分60分)1、GoC的编译+运行的快捷键是()A、F5B、F11C、F8D、F122、GoC程序的源文件扩展名是(),经过编译后生成的可执行文件扩展名是()A、CPPEXEB、EXECPP…

    2022年6月18日
  • 数据结构与算法学习笔记

    本文是王争老师的《算法与数据结构之美》的学习笔记,详细内容请看王争的专栏。有不懂的地方指出来,我做修改。数据结构与算法思维导图数据结构指的是“一组数据的存储结构”,算法指的是“操作数据的一组方法”。数据结构是为算法服务的,算法是要作用再特定的数据结构上的。最常用的数据结构预算法:数据结构:数组、链表、栈、队列、散列表、二叉树‘、堆、跳表、图、Tire树 算法:递归…

    2022年4月7日
  • 祝本站管理员孤星新婚快乐

    祝本站管理员孤星新婚快乐

    2021年7月25日
  • 深拷贝和浅拷贝的区别,说法正确的是_前端浅拷贝和深拷贝的区别

    深拷贝和浅拷贝的区别,说法正确的是_前端浅拷贝和深拷贝的区别首先,明确一点深拷贝和浅拷贝是针对对象属性为对象的,因为基本数据类型在进行赋值操作时(也就是拷贝)是直接将值赋给了新的变量,也就是该变量是原变量的一个副本,这个时候你修改两者中的任何一个的值都不会影响另一个,而对于对象或者引用数据来说在进行浅拷贝时,只是将对象的引用复制了一份,也就内存地址,即两个不同的变量指向了同一个内存地址,那么在改变任一个变量的值都是该变这个内存地址的所存储的值,所以两个变量的值都会改变。一、clone()方法在Java中是用clone()方法实现深拷贝的,比如以下代码在Jav

    2022年10月1日

发表回复

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

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