您的位置 首页 java

“全栈2019”Java多线程第二十四章:等待唤醒机制详解

难度

初级

学习时间

30分钟

适合人群

零基础

开发语言

Java

开发环境

  • JDK v11
  • IntelliJ IDEA v2018.3

友情提示

  • 本教学属于系列教学,内容具有连贯性,本章使用到的内容之前教学中都有详细讲解。
  • 本章内容针对零基础或基础较差的同学比较友好,可能对于有基础的同学来说很简单,希望大家可以根据自己的实际情况选择继续看完或等待看下一篇文章。谢谢大家的谅解!

1.温故知新

前面在 一章中介绍了 活锁(Livelock)

现在我们来讲解 等待唤醒机制

2.什么是等待唤醒机制?

这里举一个游戏的例子,帮助我们来理解什么是等待唤醒机制。

有一群参加答题的选手:

在他们面前有多种类型的题目供他们选择 ,假如有3种。选手们可自行选择答题类型:

假设他们已经选好了:

选择要作答的题型之后, 每个类型的选手们听到广播后开始抢答题名额,答题名额仅有1名,谁先占到答题桌,谁就拥有这个答题名额

机会对每个人来说都是平等的。

请问没抢到答题名额的选手呢?

没抢到答题名额的选手进去等待区,等待下一次答题机会

等正在答题的选手答题完毕,听到广播后就可以再次抢占答题名额:

至此,游戏例子演示完毕!

等待唤醒机制体现在哪呢?

“等待”体现在未抢到答题名额的选手待在等待区:

“唤醒”体现在答题区选手答题完毕之后,等待区听到广播后就可以再次同答题区选手一起抢占答题名额:

在多 线程 中,上例中的选手、作答类型、抢占答题名额、等待区、选手作答完毕、广播这些都对应着什么呢?

选手:线程

选手对应着线程,如上图中的所有选手,就好比一个个线程。

作答类型:同步锁

如上图所示,作答类型有A、B和C三种,就好比有三把同步锁lockA、lockB和lockC。

选手选择作答类型就是线程选择执行那个同步方法/代码块,然后持有相对应的同步锁

抢占答题名额:竞争同步锁

因为答题名额仅有1个,和我们的同步锁最多只能被一个线程持有一样,所以选手们抢占答题名额和多个线程竞争同一个同步锁是一回事。

等待区:没有抢占同步锁的线程暂时阻塞

没有抢到答题名额的选手们和没有竞争到同步锁的线程们一样,都将进入等待区,线程们的等待就是阻塞,暂停运行,等待持有同步锁的那个线程执行完之后,释放锁并唤醒在等待区等待获取该锁的这些线程们。

选手作答完毕:获取到同步锁的线程执行完毕

拥有答题资格的选手在作答完毕之后,会释放答题资格,然后唤醒等待区的选手们,再同他们一起抢占答题名额。

同理,持有同步锁的线程在执行完run()方法之后,会释放同步锁,然后唤醒因等待该同步锁的而阻塞的线程们,接着,这些线程们会再次竞争该同步锁。

广播:唤醒被阻塞的线程,再次竞争同步锁

拥有答题资格的选手在作答完毕之后,会释放答题资格,然后唤醒等待区的选手们,再同他们一起抢占答题名额。

此处的广播就是 告诉等待区的选手们可以再次来抢占答题名额

在线程中唤醒的实现形式有两种:notify和notifyAll。这两种分别是什么,接下来马上大家解答。

再举一个生活中的小例子

上图中,一个人正在打包货物,打包完毕之后叫另外一个人派送货物。很简单吧?

请问等待唤醒机制体现在哪?

等待体现在线程B在等待线程A任务执行完毕之后被唤醒。

唤醒体现在线程A任务执行完毕之后通知线程B可以执行任务了。

3.等待唤醒机制相关的三个方法

跟等待唤醒机制相关的三个方法: wait()、notify()、notifyAll()

等待:wait()。

通知(即唤醒):notify()、notifyAll()。

这三个方法分别定义在哪呢?

定义在Object类中:

可以清楚地看到 wait()、notify()、notifyAll()这些方法定义在Object类里面

定义在Object类里面意味着什么?

意味着 所有Java对象都拥有wait()、notify()、notifyAll()这些方法。

为什么要将wait()、notify()、notifyAll()这些方法定义Object类里面?

还记得在 一章中学习到的: 所有Java对象都可以是同步对象

既然所有Java对象都可以是同步对象,也拥有wait()、notify()、notifyAll()这些方法,这说明了一个什么问题?

说明 wait()、notify()、notifyAll()这些方法跟同步有关

就像上面刚刚演示过的动画一样:

大家都去竞争一个名额,和多个线程去竞争同一把锁是一样的。等待区的选手就像没有抢到同步锁的线程,这些线程被暂时停止运行并等待下次竞争同步锁。

所有对象都能做的事当然定义在Object类里面

wait()、notify()、notifyAll()这些方法什么意思?怎么用?

别着急,下面就来应用它们。

4.造成当前线程等待wait()方法

wait()方法在Object类中的源码:

将注释翻译成中文:

中文注释全文:

去掉注释版:

wait()方法作用是造成当前线程等待,直到它被唤醒,通常由被通知或中断。

访问权限

public :wait()方法访问权限是公开的。

final :wait()方法是最终方法,子类无法重写。

void :wait()方法没有返回值。

wait()方法只能被对象调用。

参数

无。

抛出的异常

throws IllegalMonitorStateException :如果当前线程不是对象监视器的所有者。

throws InterruptedException :如果任何线程在当前线程等待之前或当前线程中断当前线程。 抛出此异常时,将清除当前线程的中断状态。

应用

来一个线程:

为什么需要同步?

因为涉及到线程安全问题,所以需要同步。

单线程是否存在线程安全问题?

单线程不存在线程安全问题,只有两个或两个以上线程才存在线程安全问题。

综上所述,我们再来一个线程才合适:

接着,通过匿名内部类的方式实现Runnable接口:

因为wait()方法跟同步相关,所以我们得来一个同步代码块/方法:

同步代码块需要一个同步对象:

然后,在两个同步代码块里面各输出一句话,目的是为了区分线程执行的任务:

接着,在两个同步代码块里面分别调用lock.wait()方法:

wait()方法有异常抛出,因为我们是重写Runnable接口的run()方法,再加上Runnable接口的run()方法并没有抛出任何异常,所以此处wait()方法抛出的异常我们只能try-catch,不能throws

然后,在wait()方法调用完毕之后再输出一句,目的为了看当前线程是否是已经释放同步锁并暂停运行:

最后,我们启动线程即可:

至此,例子写完了,我们整理代码之后再运行程序。

演示:

请写一个wait()方法应用的例子。

请观察程序代码及结果。

代码:

Main类:

结果:

从运行结果来看,符合预期,wait()方法发挥作用。

有小伙伴说为什么程序会停在这不动?

因为我们主线程、thread1线程和thread2线程都是前台线程,即使主线程运行结束,只要还有其他前台线程没有运行结果,则整个程序就不会结束,所以看起来程序停在这里不动

不过话又说回来,thread1线程和thread2线程被wait了,什么时候可以恢复过来?

有两种情况可以让线程从阻塞状态中恢复过来:

  1. interrupt()
  2. notify()或notifyAll()

在开始下面内容之前,我们把程序代码优化一下,这两块内容重复了:

将这两块内容合并到一块去,正好也能讲一下同步方法在等待唤醒机制上的如何使用:

因为我们main()方法是一个静态方法,在main()方法内部的局部匿名内部类只能调用外部类Main中静态方法,所以say()方法必须为静态的

改写为同步静态方法之后,看程序有没有问题,运行程序,执行结果:

从运行结果来看,符合预期。

interrupt()

中断线程可以让线程从阻塞状态恢复过来。

我们来试试,这里让主线程睡3秒钟,然后再中断这两个线程:

运行程序,执行结果:

动图:

从运行结果来看,由于我们手动去中断被阻塞的线程,所以产生了中断异常:

异常发生的位置是在这:

中断被阻塞的线程让我们付出了代价,但好歹也是让我们线程醒了过来把剩余的任务给执行完:

notify()或notifyAll()

通知可以让线程从阻塞状态中恢复过来。

还是上面这个例子,只不过把interrupt()方法换成notify()或notifyAll()方法:

等等,没这么简单,这个问题等把notify()或notifyAll()例子演示完了再解答, 总之不可以这样写

那该如何调用notify()或notifyAll()方法呢?

应该 让主线程拥有和线程thread1和thread2一样的同步锁,然后在同步代码块里面调用notify()或notifyAll()方法即可

运行程序,执行结果:

从运行结果来看,好像只有一个线程醒过来了,另一个线程没有醒过来。

这是为什么呢?

因为notify()方法只唤醒正在此对象监视器上等待的单个线程,而且还是随机的,所以只有一个线程被唤醒

对象监视器 ”是什么?

这个我们在 一章中讲过,就是 同步锁

如果想唤醒同步锁上等待的所有线程怎么做?

调用notifyAll()方法即可。

我们来试试,将notify()方法换成notifyAll()方法:

运行程序,执行结果:

从运行结果来看,程序现在变得非常完美,因为所有线程都醒过来了。

IllegalMonitorStateException异常

刚刚有一个问题没有解决,就是 为什么不能直接调用lock.notify()/lock.notifyAll(),而是要写在同步代码块/方法里面

因为无论是wait()还是notify()、notifyAll()方法都需要当前线程是对象监视器的所有者,而线程必须有同步锁才能去执行同步代码块/方法里面的内容,所以同步对象调用wait()、notify()和notifyAll()方法需要写在同步代码块/方法里面。

这里可以试试没有同步锁的时候去调用同步对象的wait()、notify()和notifyAll()方法:

运行程序,执行结果:

从运行结果来看,程序产生了IllegalMonitorStateException异常。

什么是IllegalMonitorStateException异常?

IllegalMonitorStateException翻译过来就是:非法监视状态异常。 如果当前线程不是对象监视器的所有者则会产生IllegalMonitorStateException异常。

希望大家可以注意这点。

wait()重载方法

从wait()方法源码中,我们发现wait()方法内部其实是调用了wait(0L)方法(即永远等待被唤醒,不主动醒来):

不过,在wait​(long timeoutMillis)方法的下方我们还发现了wait()的重载方法wait​(long timeoutMillis, int nanos):

这样一来,wait()的重载方法有两个:

  • wait​(long timeoutMillis)
  • wait​(long timeoutMillis, int nanos)

下面就来一一介绍它们。

5.造成当前线程等待并超时自动恢复 wait​(long timeoutMillis) 方法

wait​(long timeoutMillis)方法在Object类中的源码:

将注释翻译成中文:

中文注释全文:

造成当前线程等待,直到它被唤醒,通常由被通知或中断,或超时。

去掉注释版:

wait​(long timeoutMillis)方法作用是造成当前线程等待,直到它被唤醒,通常由被通知或中断,或超时。

访问权限

public :wait​(long timeoutMillis)方法访问权限是公开的。

final :wait​(long timeoutMillis)方法是最终方法,子类无法重写。

native :wait​(long timeoutMillis)方法具体是用本地方法实现的。

void :wait​(long timeoutMillis)方法没有返回值。

wait​(long timeoutMillis)方法只能被对象调用。

注:

native关键字:用来声明一个方法是由与计算机相关的语言(如C/C++/FORTRAN语言)实现的

参数

timeoutMillis :等待的最长时间,以毫秒为单位。

抛出的异常

throws IllegalArgumentException :如果timeoutMillis为负数。

throws IllegalMonitorStateException :如果当前线程不是对象监视器的所有者。

throws InterruptedException :如果任何线程在当前线程等待之前或当前线程中断当前线程。 抛出此异常时,将清除当前线程的中断状态。

应用

wait​(long timeoutMillis)方法是有一个timeoutMillis参数的,我们可以设置线程等待的最长时间,以毫秒为单位,超过这个时间线程就会从阻塞状态中恢复。

下面来试试,还是上一小节的例子,将wait()方法改为了wait​(long timeoutMillis)方法:

线程等待的最长时间为1000毫秒,即1秒。

运行程序,执行结果:

从运行结果来看,两个线程在没有手动唤醒的情况下自动醒了,符合预期。

注意:

我们的timeoutMillis(等待的最长时间)不能为负数,如果为负数则会产生IllegalArgumentException异常。

6.造成当前线程等待并超时自动恢复 wait​(long timeoutMillis, int nanos) 方法

wait​(long timeoutMillis, int nanos)方法在Object类中的源码:

中文注释全文:

造成当前线程等待,直到它被唤醒,通常由被通知或中断,或超时。

去掉注释版:

wait​(long timeoutMillis, int nanos)方法作用是造成当前线程等待,直到它被唤醒,通常由被通知或中断,或超时。

访问权限

public :wait​(long timeoutMillis, int nanos)方法访问权限是公开的。

final :wait​(long timeoutMillis, int nanos)方法是最终方法,子类无法重写。

void :wait​(long timeoutMillis, int nanos)方法没有返回值。

wait​(long timeoutMillis, int nanos)方法只能被对象调用。

参数

timeoutMillis :等待的最长时间,以毫秒为单位。

nanos :额外的时间,以纳秒为单位,范围为0-999999(含)。

抛出的异常

throws IllegalArgumentException :如果timeoutMillis为负数,或者nanos的值超出范围。

throws IllegalMonitorStateException :如果当前线程不是对象监视器的所有者。

throws InterruptedException :如果任何线程在当前线程等待之前或当前线程中断当前线程。 抛出此异常时,将清除当前线程的中断状态。

应用

wait​(long timeoutMillis, int nanos)方法和sleep​(long millis, int nanos)方法很相似,其实在内部的一些代码规则很一样的,感兴趣的小伙伴可以前去阅读看看,在 一章中。

wait​(long timeoutMillis, int nanos)方法参数注意要点。

第一个,timeoutMillis不能小于0,否则会产生IllegalArgumentException异常

第二个,nanos不能小于0或大于999999,否则会产生IllegalArgumentException异常

第三个,只需nanos大于0,timeoutMillis数值就加1

以上3点需要大家注意下,另外最后一点再强调一下,不是累加nanos的值,是判断nanos是否大于0,而且累加的还是timeoutMillis的值。

好了,注意点说完了,接下来我们来看看怎么用。

还是上一小节的例子,只不过要把wait​(long timeoutMillis)方法换成wait​(long timeoutMillis, int nanos)方法:

运行程序,执行结果:

从运行结果来看,符合预期。

wait​(long timeoutMillis, int nanos)使用起来很简单,多注意参数nanos就可以了。

7.唤醒单个等待线程notify()方法

notify()方法在Object类中的源码:

中文注释全文:

唤醒正在此对象监视器上等待的单个线程。 如果任何线程正在等待此对象,则选择其中一个线程被唤醒。 选择是任意的。 线程通过调用wait方法来在一个对象的监视器上进行等待。

被唤醒的线程将无法继续运行,直到当前线程放弃此对象的锁。 被唤醒的线程将以正常的方式与其他线程在同一个锁上展开竞争,没有任何优势和劣势。

去掉注释版:

notify()方法作用是唤醒正在此对象监视器上等待的单个线程。

访问权限

public :notify()方法访问权限是公开的。

final :notify()方法是最终方法,子类无法重写。

native :notify()方法具体是用本地方法实现的。

void :notify()方法没有返回值。

notify()方法只能被对象调用。

注:

native关键字:用来声明一个方法是由与计算机相关的语言(如C/C++/FORTRAN语言)实现的

参数

无。

抛出的异常

throws IllegalMonitorStateException :如果当前线程不是对象监视器的所有者。

应用

还是上一小节的例子,不过我们要把wait​(long timeoutMillis, int nanos)方法换成wait()方法, 因为这次不让正在等待的线程自动恢复,而是调用notify()方法手动恢复,所以要换一个方法

其次,还是让主线程睡眠3秒钟:

因为notify()方法要在当前线程拥有同步锁的环境内使用,所以这里我们可以在同步代码块/方法中来使用notify()方法

运行程序,执行结果:

从运行结果来看,好像只有一个线程醒过来了,另一个线程没有醒过来。

这是为什么呢?

因为notify()方法只唤醒正在此对象监视器上等待的单个线程,而且还是随机的,所以只有一个线程被唤醒

如果想唤醒所有正在此对象监视器上等待的线程则需要使用notifyAll()方法。

8.唤醒所有等待线程notifyAll()方法

notifyAll()方法在Object类中的源码:

中文注释全文:

唤醒正在此对象监视器上等待的所有线程。

去掉注释版:

notifyAll()方法作用是唤醒正在此对象监视器上等待的所有线程。

访问权限

public :notifyAll()方法访问权限是公开的。

final :notifyAll()方法是最终方法,子类无法重写。

native :notifyAll()方法具体是用本地方法实现的。

void :notifyAll()方法没有返回值。

notifyAll()方法只能被对象调用。

参数

无。

抛出的异常

throws IllegalMonitorStateException :如果当前线程不是对象监视器的所有者。

应用

notifyAll()方法和notify()方法唯一区别就在与:notifyAll()方法是 唤醒正在此对象监视器上等待的所有线程。 而notify()方法是 唤醒正在此对象监视器上等待的单个线程。

接下来,我们还是使用上一小节的例子,只不过将notify()方法换成notifyAll()方法:

运行程序,执行结果:

从运行结果来看,符合预期。所有正在此对象监视器上等待的线程都被唤醒了。

GitHub

本章程序GitHub地址:

总结

  • wait()、notify()、notifyAll()这些方法定义在Object类里面,意味着所有Java对象都拥有wait()、notify()、notifyAll()这些方法。
  • wait()方法造成当前线程在等待,直到它被唤醒,通常由被通知或中断。
  • notify()方法作用是唤醒正在此对象监视器上等待的单个线程,选择是随机的。
  • notifyAll()方法作用是唤醒正在此对象监视器上等待的所有线程。

至此,Java中等待唤醒机制相关内容讲解先告一段落,更多内容请持续关注。

答疑

如果大家有问题或想了解更多前沿技术,请在下方留言或评论,我会为大家解答。

上一章

下一章

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

学习小组

加入同步学习小组,共同交流与进步。

  • 方式一:关注头条号Gorhaf,私信“Java学习小组”。
  • 方式二:关注公众号Gorhaf,回复“Java学习小组”。

全栈工程师学习计划

关注我们,加入“全栈工程师学习计划”。

版权声明

原创不易,未经允许不得转载!

文章来源:智云一二三科技

文章标题:“全栈2019”Java多线程第二十四章:等待唤醒机制详解

文章地址:https://www.zhihuclub.com/180684.shtml

关于作者: 智云科技

热门文章

网站地图