【JAVA进化论】LV5-4:线程通信:wait、notify、notifyall
一、Object类里对线程实施控制的方法
前面说过,Object
是所有类的父类,它自带equals
、hashCode
等方法,我们这次来介绍几个新的成员方法:
这节要讲的wait
、notify
、notifyall
都是Object里自带的方法,通过这些方法可以控制当前正在运行这个对象的线程,如何控制?往下看~
二、利用线程控制方法完成线程通信
上述几个方法是属于每个实例对象的,所有实例都拥有一个“等待队列”(虚拟概念,实例里并不存在该字段),它是在实例的wait方法调用后存放停止操作线程的队列。执行wait方法后,线程进入当前实例的“等待队列”,以下几种情况可以让线程退出“等待队列”:
- 其他线程调用
notify
、notifyAll
方法来将其唤醒
- 其他线程调用
interrupt
来将其唤醒
wait
方法本身超时
2.1:wait
当执行了下面的代码:
1 | obj.wait(); |
我们可以说当前线程在obj上发生了等待,当前线程进入了obj的“等待队列”,此时当前线程会让出锁,让其他线程继续竞争获得该实例的锁(因此这里有个规则,调用wait的线程必须持有当前实例对象的锁)
⚠️ 普通不加synchronized的方法也可以使用wait,这在编译器是没问题的,但是运行期会报
IllegalMonitorStateException
异常。
还是以前面提过的Word对象为例,但是现在A这个方法里调用了wait方法,这时多线程访问时过程就变成了下图:
2.2:notify
现在先来介绍下notify
,该方法会将等待队列里的线程取出,让其退出等待并参与锁竞争然后继续执行上次wait后没有执行完的语句。整体过程如下图所示:
可以看到,t1在被挂起后,会因为t2调用了同实例的notify方法,而让t1被从等待队列里释放,重新加入到所得竞争力,t2执行完毕后释放锁,锁又再次被t1竞争到,t1将继续执行上次被挂起时后面未执行完的语句。
需要指出的是,如果等待队列里的线程是多个,那么被唤醒的那一个,将会是等待队列里所有线程随机的一个,不会特定哪一个线程会被唤起。
2.3:notifyAll
接下来介绍notifyAll
方法,顾名思义,就是将等待队列里的线程全部唤起
,然后这些线程将全部加入到锁竞争,竞争到,继续完成上次被挂起时未执行完毕的操作,流程图如下:
说明,当线程调用实例的wait
、notify
、notifyAll
方法有个大前提,就是必须要求该线程拥有
该实例的锁
,否则会抛IllegalMonitorStateException
异常。
在编写程序时,是该选择notify还是选择notifyAll?这个可以指出的是,notifyAll往往更加健壮
,而notify由于唤起的线程少,因此效率会更高,但是存在程序停止的风险
。
三、实例
3.1:wait+notify的简单例子
以图4
里的Word对象为例,我们将这个Word类定义出来:
1 | public class Word { |
测试类,我们开启两个线程来访问同一个word对象里的同步方法:
1 | public static void main(String[] args) throws Exception { |
这个运行结果为:
1 | A方法 wait触发前 |
看到了吗?明明t1抢到了对象锁,但是只运行了A方法wait前面的部分,这时因为运行到wait时被挂起,释放锁,然后1s后t2抢到锁,运行B方法,然后触发B方法里的notifyAll后,所有放到w1对象等待队列里的线程被唤起,重新加入到锁竞争里去,此时t1再次竞争到w1的对象锁,然后把自己没执行完的方法栈执行完。
3.2:利用线程通信实现阻塞队列
大体流程如下:
简单来说,就是有一个公共缓冲带
,我们管它叫做消息队列
,可以由多个生产者往它里面生产数据,可以由多个消费者获取,当队列内无消息可读时,所有消费者陷入等待,等待新的消息到来:
根据这个结构的特性,我们可以利用本节所学wait
和notify
来编写。
定义阻塞队列类,根据我们上面的描述,一个组的队列应该具备生产和获取的能力:
1 | //阻塞队列类 |
我们来开启几个独立的线程来当成是生产者程序和消费者程序,分别对BlockQueue对象进行生产-消费的操作:
1 | public static void main(String[] args) throws Exception { |
运行结果:
这个流程,如果对线程通信还不是很熟悉的话就需要仔细分析一下😁