Java线程间通讯——等待通知机制
在JDK中实现线程同步等待闭环(FutureTask/Future)中已经实现了线程之间的同步等待。具体如何实现的呢?需要分析具体方式具体代码,如下:
1、join()方法
2、Future/FutureTask
以上两图中都有wait字眼,即等待线程结束。这种Java中对多线程执行顺序使用等待/通知控制的方式也称为阻塞/唤醒机制:是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
那么是同步呢?同步是指程序中用于控制不同线程间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。开发人员必须显示指定某个方法或某段代码需要在线程之间互斥执行。
以上同步等待过程中线程之间如何通信的呢?这里的通信是指线程之间以何种机制来交换信息。在共享内存的并发模型里,线程之间共享程序的公共状态(在JVM运行时内存管理之线程共享详细介绍了线程共享内存),通过写-读内存中的公共状态进行隐式通信。
Java的并发选择采用的就是共享内存模型JMM,即隐式通讯显式同步(如上图两种方式的同步等待方式)。
等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object上:
先通过单线程(生产者和消费者)使用该机制实践,从结果看:先生产后消费不会出现没有生产就消费的情况。具体代码和测试结果如下:
产品流水线(自定义队列):
生产者:
消费者:
再看多线程(生产者和消费者分别多线程):
再执行一次:
多线程每次执行的结果都不一样,也不是先生产后消费。wait和notify在此多线程应用不失效了?!从通知等待机制来分析,多线程的案例中10个线程分两组分别调用了对象myQueue中的wait()和notify(),相互通讯不应该出现失效的情况,为什么还是会出现消费者线程没有等到生产者的通知已生产完毕再进行消费的情况?
分析源代码,通知等待机制哪里失控了?首先分析消费者线程:
消费者线程从下标0开始获取消息队列中的数据,下标(getIndex)增加自加1(如果下标等于队列长度,则下标从0重新开始);同时队列长度(size)减1,如果消息队列长度为0时wait()——等待生产者。生产者线程数据状态正好与之相反。
size作为线程共享数据放在元数据区,任意线程的get或put方法都可以随机时间sleep之后对其进行修改。失控的点就在此处:当size小于等于0时,并没有有效阻塞消费者进行数据的获取——因为第一次阻塞成功以后就直接进行了相关的操作,而多线程中每次操作前都得进行阻塞、等待、通知的操作。
综合上述,多线程并发如果只是有关键字synchronized,wait(),notify()等方法并不能完全保证业务逻辑的正确性即有效同步。怎么解决这个问题呢?还需要合理的业务逻辑:
与单线程对比:
get方法做类似的调整——代码基本没有变化,只是利用递归及非你即我的增强了抢对象锁的逻辑。
以上可以得出一个小经验:提炼出等待/通知的经典范式——加锁,循环和处理逻辑,该范式分为两部分,分别针对等待方(消费者)和通知方(生产者)。
等待方遵循如下原则:
1)获取对象的锁。
2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。
对应的伪代码如下:
synchronized(对象) {
while(条件不满足) {
对象.wait();
}
对应的处理逻辑
}
通知方遵循如下原则:
1)获得对象的锁。
2)改变条件。
3)通知所有等待在对象上的线程。
对应的伪代码如下:
synchronized(对象) {
改变条件
对象.notifyAll();
}