白话说 Java 线程之等待、通知机制

正文共: 2760字 5图

预计阅读时间: 7分钟

一、前言

在程序中,线程存在的意义就是高效的完成某项任务,但是都是以一个独立的个体存在的,也就是说,如果不经过特殊处理,一个线程只能孤独的完成自己被赋予的任务,然后在完成任务之后,自我销毁。

如果能让多个线程之间进行通信协作,就可以更高效的完成任务。这就好像个人和团队的区别,团队如果配合得当,必定比个人完成工作的效率更高。那么,让多个线程之间通信,就是项协作的基本需求。

在 Java 中,线程间通信一般会使用等待 (wait)、通知 (noticy) 的机制,接下来就围绕这个机制模型来进行讲解。

二、Java的等待/通知机制

1、比较low的方法

其实 Java 中是提供了线程间通信的机制,但是假如我们事先并不知道存在这样的机制,会怎么设计让多个线程间协作。

首先想到的应该是使用 while(true) + sleep() 的模型来做等待机制,在 while 循环里,判断某个共享变量是否满足条件,如果满足继续执行,如果不满足,使用 sleep() 控制等待,然后继续判断条件是否满足。

.

虽然使用 while() + sleep() 的方式,可以在多线程之间实现了通信,但是有一个弊端,就是 OneThread 这个线程,会不停的使用轮询的方式判断条件是否满足,这样会非常的浪费 CPU 的资源。

而且轮询的间隔非常难把握,如果时间太短,在条件不满足之前,都是在空循环,而时间间隔太长又没有办法及时的获取到状态的改变。

2、Java自己的等待、通知机制

既然使用 while() + sleep() 的方式非常的“不环保”,那么如何利用 Java 自己的等待、通知机制来完成上例子在中,线程等待的机制呢。

先简单说说,等待、通知机制,其实在生活中非常的常见,举个常见的例子:

厨师和传菜员,厨师做完一道菜的时间是不确定的,那么如果有客人来吃饭,点了菜之后,厨师就开始做菜。在轮询的场景下,就是传菜员间隔五分钟过来后厨问一下:“菜做好了吗?”,没做好继续等五分钟再来问。而如果有更优雅的方式,厨师说,你也别五分钟来问一次了,怪累的,这样,菜做好了,我叫你。

这个优雅的方式,就是:好了,我叫你

在 Java 中,使用这种优雅的方式,需要借助两个方法:

  • wait():让当前线程,立即放弃锁,进入等待状态,直到其它获取锁的线程调用notify()方法再重新争抢锁。

  • notify():通知其它进入 wait() 的线程,可以继续运行了。

在使用的过程中,有几点需要注意:

  1. 这两个方法,都是 Object 类的方法,表示在当前的锁上进行等待和通知操作,也就是调用wait()notify()的对象,必须是加锁的对象。

  2. 这两个方法,都需要在已经获取到线程锁的情况下,在同步代码块内,才可以调用,否者会抛出IllegalMonitorStateException这个异常。

  3. wait() 会立即放弃锁,进入等待状态。而 notify() 并不会立即放弃锁,它是会等同步代码块执行完成才放弃锁并通知其它线程。

接下来我们来改写上面的方法,使用 wait() 等待,notify() 通知的方式。

从上面的例子,验证了我上面总结的内容,notify()并不立即释放锁,而是会优先执行完当前的同步代码,再释放锁并通知出去。

3、wait/notify的缺陷

简单的 wait/notify 的机制是有缺陷的,wait 必须等其他线程去 notify 才可以生效,而如果其他线程在此后并不来调用 notify() 的话,可能永远被等待下去,永远也得不到执行,然后就会发现线程“假死“了。

而在更多线程需要调用 wait() 或者 notify() 的时候,notify()只会随机的通知一个已经处于 wait() 状态的线程,去继续执行,而无法通知到所有处于等待状态的线程。

所以 Java 其实还提供了一些其它的 API 来解决这些问题:

  • wait(timeout):同样是等待,但是它设定了超时的机制,也就是说如果超过这个时间还没有被 notify 的话,会自动取消掉 wait 状态,继续去争抢锁得到执行权。

  • notifyAll():和名字一样,它会去通知所有处于 wait 状态的线程,可以开始执行了,但是如果有多个线程处于 wait 状态,也是需要争抢锁才能继续执行。

4、wait/notify机制的原理

从上面的讲解中也可以了解到,其实就是在不同的状态下相互切换。而每个锁对象,都会维护两个队列,一个是就绪队列,记录了将要获取锁的线程,处于就绪状态,获取到锁就可以立即执行,另外一个是阻塞队列,在阻塞队列中记录了等待被唤醒的线程,处于 wait、sleep 等状态的,当他们被唤醒之后,才会进入到就绪队列,等待分配锁资源后继续执行。

在有锁的情况下,大概运行的流程是这个样子的。

三、最后再举例子

在多线程的例子中,生产者、消费者真的是一个非常经典的例子。这里同样使用这个例子来说明问题。

生成者和消费者的例子,简单来说,就是一部分线程用于生成事件,而另外一部分线程用于消费事件。

1、单一生产者和消费者

没什么好说的,直接上例子。

上面的例子中,生产者负责生产一个字符串,然后消费者将其置空,同时都输出Log,输出结果如下。

从Log中可以看到,生产者和消费者因为都是单一的,所以是生产一个就通知消费者消费一个。

2、多生产者、多消费者

上面在单一生产者和消费者的环境中,生产的速度和消费的速度是匹配的,这样的情况下,可以完美的一直运行下去。但是如果处于多生产者和消费者的情况下,会出现什么情况?

因为 notify 只会让一个 wait 的线程被现场调度器启动并执行,那么如果有多个生产者和消费者的话,可能一直是某一方单边被激活,那么就会多生产少消费或者少生产多消费,生产效率和消费效率不匹配的情况,其实是不利于效率的,总会有单边的线程被停滞。再极端一点的情况,每次都命中同一边,这样的话,就没有生产者或者没有消费者了。

那么如何避免这样的问题?其实使用 wait(timeout) 或者 notifyAll() 都可以解决这样的问题,因为全部启动,大家每次都公平的争抢锁,基本上就不会存在只通知某一个而导致效率不匹配的问题,设置 wait 的超时时间,同样也可以保证所有的线程都有机会被重新进入就绪队列中。

这里就不再提供例子了,有兴趣的可以把上面的例子,启动多个生产者或者消费者看看效果。

今天在承香墨影公众号的后台,回复『成长』。我会送你一些我整理的学习资料,包含:Android反编译、算法、设计模式、虚拟机、Linux、Kotlin、Python、爬虫、Web项目源码。

推荐阅读:

听说喜欢留言的人,运气都不会太差

点击『阅读原文』查看更多精彩内容

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值