设为首页 加入收藏

TOP

Java 生产者消费者模式详细分析(一)
2018-01-17 13:05:40 】 浏览:592
Tags:Java 生产者 消费者 模式 详细 分析

本文目录:
1.等待、唤醒机制的原理
2.Lock和Condition
3.单生产者单消费者模式
4.使用Lock和Condition实现单生产单消费模式
5.多生产多消费模式(单面包)
6.多生产多消费模式


生产者消费者模式是多线程中最为常见的模式:生产者线程(一个或多个)生成面包放进篮子里(集合或数组),同时,消费者线程(一个或多个)从篮子里(集合或数组)取出面包消耗。虽然它们任务不同,但处理的资源是相同的,这体现的是一种线程间通信方式。


本文将先说明单生产者单消费者的情况,之后再说明多生产者多消费者模式的情况。还会分别使用wait()/nofity()/nofityAll()机制、lock()/unlock()机制实现这两种模式。


在开始介绍模式之前,先解释下wait()、notify()和notifyAll()方法的用法细节以及改进的lock()/unlock()、await()/signal()/signalAll()的用法。


 


wait()、notify()和notifyAll()分别表示让线程进入睡眠、唤醒睡眠线程以及唤醒所有睡眠的线程。但是,对象是哪个线程呢?另外,在API文档中描述这三个方法都必须在有效监视器(可理解为持有锁)的前提下使用。这三个方法和锁有什么关系呢?


以同步代码块synchronized(obj){}或同步函数为例,在它们的代码结构中可以使用wait()、notify()以及notifyAll(),因为它们都持有锁。


对于下面的两个同步代码块来说,分别使用的是锁obj1和锁obj2,其中线程1、线程2执行的是obj1对应的同步代码,线程3、线程4执行的是obj2对应的同步代码。


当t1开始执行到wait()时,它将进入睡眠状态,但却不是一般的睡眠,而是在一个被obj1标识的线程池中睡眠(实际上是监视器对应线程池,只不过此时的监视器和锁是绑定在一起的)。当t2开始执行,它发现锁obj1被其他线程持有,它将进入睡眠态,这次睡眠是因为锁资源等待而非wait()进入的睡眠。因为t2已经判断过它要申请的是obj1锁,因此它也会进入obj1这个线程池睡眠,而不是普通的睡眠。同理t3和t4,这两个线程会进入obj2线程池睡眠。


当某个线程执行到notify()时,这个notify() 随机 唤醒它 所属锁对应线程池 中的 任意一个 线程。例如,obj1.notify()将唤醒obj1线程池中任意一个睡眠的线程(当然,如果没有睡眠线程则什么也不做)。同理notifyAll()则是唤醒所属锁对应线程池中所有睡眠的线程。


必须要搞清楚的是"对应锁",因为在调用wait()、notify()和notifyAll()时都必须明确指定锁。例如,obj1.wait()。如果省略了所属锁,则表示的是this这个对象,也就是说,只有在非静态的同步函数中才能省略这三个方法的前缀。


简而言之,当使用了同步,就使用了锁,线程也就有了归属,它的所有依据都由所属锁来决定。例如,线程同步时,判断锁是否空闲以决定是否执行后面的代码,亦决定是否去特定的线程池中睡眠,当唤醒时也只会唤醒所属锁对应线程池中的线程。


这几个方法在应用上,一般在一次任务中,wait()notify()/notifyAll()成对出现且择一执行的。换句话说,就是这一轮原子性同步执行过程中,要么执行wait()进入睡眠,要么执行notify()唤醒线程池中的睡眠线程。要如何实现择一执行,可以考虑使用标记的方式来作为判断依据。参考后文的例子。


 


wait()系列的三个方法局限性很大,因为无论是睡眠还是唤醒的动作,都完全和锁耦合在一起了。例如,锁obj1关联的线程只能唤醒obj1线程池中的线程,而无法唤醒锁obj2关联的线程;再例如,在原来synchronized同步时,锁是在开始同步时隐式地自动获取的,且是在执行完一整个任务后,又隐式地自动释放锁,也就是说获取锁和释放锁的动作无法人为控制。


从JDK 1.5开始,java提供了java.util.concurrent.locks包,这个包中提供了Lock接口、Condition接口和ReadWriteLock接口,前两个接口将锁和监视器方法(睡眠、唤醒操作)解耦了。其中Lock接口只提供锁,通过锁方法newConditon()可以生成一个或多个与该锁关联的监视器,每个监视器都有自己的睡眠、唤醒方法。也就是说Lock替代了synchronized方法和同步代码块的使用,Condition替代了Object监视器方法的使用。


如下图:



当某线程执行condition1.await()时,该线程将进入condition1监视器对应的线程池睡眠,当执行condition1.signal()时,将随机唤醒condition1线程池中的任意一个线程,当执行condition1.signalAll()时,将唤醒condition1线程池中的所有线程。同理,对于condition2监视器也是一样的。


即使有多个监视器,但只要它们关联的是同一个锁对象,就可以跨监视器操作对方线程。例如condition1中的线程可以执行condition2.signal()来唤醒condition2线程池中的某个线程。


要使用这种锁、监视器的关联方式,参考如下步骤:


具体用法见后文关于Lock、condition的示例代码。


 


一个生产者线程,一个消费者线程,生产者每生产一个面包放进盘子里,消费者从盘子里取出面包进行消费。其中生产者判断是否继续生产的依据是盘子里没有面包,而消费者判断是否消费的依据是盘子里有面包。由于这个模式中,盘子一直只放一个面包,因此可以把盘子省略掉,生产者和消费者直接手把手地交递面包即可。


首先需要描述这三个类,一是多线程共同操作的资源(此处即面包),二是生产者,三是消费者。在下面的例子中,我把生产面包和消费面包的方法分别封装到了生产者和消费者类中,如果把它们封装在面包类中则更容易理解。


最后的执行结果应当生产一个、消费一个,如此不断循环。如下:


 


代码如下:


 


这里先说明多生产者多消费者,但同一个时刻最多只能有一个面包的模式,这个模式在实际中可能是不理想的,但为了引出后面真实的多生产多消费模式,我觉得有必要在这里解释这种模式,并且分析这种模式以及如何从单生产单消费的代码演变而来。


如下图:



从单生产单消费到多生产多消费,因为多线程安全问题和死锁问题,所以有两个方面的问题需要考虑:


其实从单线程到多线程,就两个问题需要考虑:不同步和死锁。(1)当生产方和消费方都出现了多线程,可以将生产方的多线程看成一个线程整体、消费方的多线程也看成一个整体,这解决的是线程安全问题。(2)再将生产方整体和消费方整体两方结合起来看成多线程,来解决死锁问题,而java中解决死锁的方式就是唤醒对方或唤醒所有。


问题是如何保证某一方的多线程之间同步?以多线程执行单消费方的代码为例进行分析。


假设消费线程1消费完一个面包后唤醒了消费线程2,并继续循环,判断if(!flag),它将wait,于是锁被释放。假设CPU正好选中了消费线程2,那么消费线程2也将进入wait。当生产方生产了一个面包后,假设唤醒了消费线程1,它将从wait语

首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇RabbitMQ消息分发轮询和Message A.. 下一篇Python字符编码详细分析

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目