版权声明:
本账号发布文章均来自公众号,承香墨影(cxmyDev),版权归承香墨影所有。
允许有条件转载,转载请附带底部二维码。
一、前言
在程序中,线程存在的意义就是高效的完成某项任务,但是都是以一个独立的个体存在的,也就是说,如果不经过特殊处理,一个线程只能孤独的完成自己被既定的任务,然后完成任务之后,自我销毁。
如果能让多个线程之间进行通信协作,就可以跟高效的完成任务,这就是个人和团队的区别,团队如果配合得当,必定比个人的效率更高。那么,让多个线程之间通信,就是项基本的需求。
在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()的线程,可以继续运行了。
在使用的过程中,有几点需要注意:
- 这两个方法,都是Object类的方法,表示在当前的锁上进行等待和通知操作,也就是调用
wait()
和notify()
的对象,必须是加锁的对象。 - 这两个方法,都需要在已经获取到线程锁的情况下,在同步代码块内,才可以调用,否者会抛出
IllegalMonitorStateException
这个异常。 -
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的超时时间,同样也可以保证所有的线程都有机会被重新进入就绪队列中。
这里就不再提供例子了,有兴趣的可以把上面的例子,启动多个生产者或者消费者看看效果。