当前位置: 首页 > 后端技术 > Java

访谈突击24:wait和notify为什么一定要放在synchronized里?

时间:2023-04-01 22:39:18 Java

在多线程编程中,wait方法是让当前线程进入休眠状态,直到另一个线程调用notify或notifyAll方法后才继续恢复执行。在Java中,wait和notify/notifyAll都有自己的格式要求,即在使用wait和notify时(notifyAll的用法与notify类似,所以下面只会用notify来指代两者)必须配合synchronized才行一起使用它们。wait/notify基础使用wait和notify的基本方法如下:Objectlock=newObject();newThread(()->{synchronized(lock){try{System.out.println("beforewait");//调用等待方法lock.wait();System.out.println("afterwait");}catch(InterruptedExceptione){e.printStackTrace();}}}).start();Thread.sleep(100);synchronized(lock){System.out.println("执行通知");//调用notify方法lock.notify();}上面代码的执行结果如下图所示:wait/notify和synchronized一起用?那么问题来了,wait和notify一定要和synchronized一起用吗?wait和notify可以单独使用吗?我们尝试把上面代码中的synchronized代码行删掉,实现代码如下:乍一看,代码似乎没问题,编译器也不报错,似乎“可以正常使用”.但是,当我们运行上面的程序时,出现如下错误:从上面的结果可以看出,无论是wait还是notify,如果不和synchronized一起使用,程序运行的时候会报IllegalMonitorStateException,并报非法monitorstate异常,notify无法实现程序的唤醒功能。原因分析从上面的报错信息可以看出,JVM在运行时会强制检查同步代码中是否有wait和notify。如果不是,就会报非法监控状态异常(IllegalMonitorStateException),但这只是运行程序时的表象,那么Java为什么要这样设计呢?其实之所以这样设计,是为了防止多线程并发运行时程序执行混乱。乍一看,这句话好像是用来形容“锁”的。然而,实际情况也是如此。wait和notify中引入锁是为了避免并发执行时程序执行混乱的问题。那么这个“执行混乱问题”到底是什么?接下来我们继续往下看。重现了waitandnotify的问题。我们假设wait和notify可以解锁,我们用它们来实现一个自定义的阻塞队列。这里的阻塞队列指的是读操作的阻塞,即读数据时,如果有数据就返回数据,如果没有数据就阻塞等待数据。实现代码如下:classMyBlockingQueue{//用于保存数据的集合Queuequeue=newLinkedList<>();/***添加方法*/publicvoidput(Stringdata){//添加数据到队列queue.add(data);//唤醒线程继续执行(这里的线程指的是执行take方法的线程)notify();//③}/***获取方式(阻塞执行)*如果队列中有数据,则返回数据,如果没有数据,则阻塞等待数据*@return*/publicStringtake()throwsInterruptedException{//使用while判断是否有数据(使用while而不是if是为了防止误唤醒)while(queue.isEmpty()){//①//如果没有任务,先block然后等待();//②}返回队列.remove();//返回数据}}注意上面的代码,我们确定了代码中三个关键的执行步骤:①:判断队列中是否有数据;②:执行等待睡眠操作;③:向队列中添加数据,唤醒阻塞线程。如果不强制添加synchronized,那么会出现如下问题:step线程1线程21执行step①判断当前队列没有数据2执行step③向队列中添加数据,唤醒线程1进行继续执行3执行步骤②线程1进入休眠从上面的执行流程看出问题了吗?如果wait和notify不强制加锁,那么线程1执行完判断后,sleep前,另一个线程向队列中添加数据。但是此时线程1已经执行了判断,所以会直接进入休眠状态,导致队列中的数据永久不可读。这就是程序并发运行时“执行结果混乱”的问题。但是如果和synchronized一起使用的话,代码会变成这样:classMyBlockingQueue{//Queue存放任务的队列/***Add方法*/publicvoidput(Stringdata){synchronized(MyBlockingQueue.class){//添加数据到队列queue.add(data);//为了防止take方法阻塞sleep,需要调用wakeup方法notifynotify();//③}}/***获取方式(阻塞执行)*如果队列中有数据,则返回数据,如果没有数据,则阻塞等待数据*@return*/publicStringtake()throwsInterruptedException{synchronized(MyBlockingQueue.class){//使用while判断是否有数据(使用while代替if是为了防止误唤醒)while(queue.isEmpty()){//①//没有任务,先阻塞等待wait();//②}}返回队列。消除();//返回数据}}经过这样的改造,关键的步骤①和②可以一起执行,这样线程执行完步骤③后,线程1就可以读取队列中的数据了,它们的执行过程如下:步骤线程1线程21执行步骤①判断当前队列没有数据2执行步骤②线程进入休眠状态3执行步骤③向队列添加数据并执行唤醒操作4线程被唤醒继续执行5判断有数据在队列中,像这样返回数据我们的程序是可以正常执行的,这也是Java设计为什么一定要让wait和notify和synchronized一起使用的原因。总结本文介绍了wait和notify的基本使用,以及为什么wait和notify/notifyAll必须配合synchronized使用的原因。如果不强制wait和notify/notifyAll和synchronized一起使用,那么在多线程执行时,会执行一半wait,然后执行添加数据和notify的操作,会导致线程休眠每时每刻。判断是非在己,名誉在人,得失在数。公众号:Java面试真题分析面试合集:https://gitee.com/mydb/interview