生产者与消费者模型
笔试考了,Mark一下。
在平时的编程中,经常遇到一个线程要产生数据,而另一个线程要处理产生出来的数据,这其实就是生产者和消费者的关系。生产者在产生数据后可以直接调用消费者处理数据;也可以把数据放在一个缓冲区中,让消费者从缓冲区中取出数据处理,两种方式从调用方式上来说,第一种可是说是同步的,即生产者在生产出数据后要等待消费者消耗掉后才能生产下一个数据,等待时间的长短取决于消费者处理数据的能力;第二种方式是异步的,生产者只管生产数据,然后扔到一个缓冲区内,不管数据是否被立即处理了,消费者则从缓冲区中依次取出数据进行自己节奏的处理。从线程模型角度来说,第一种是单线程的,而第二种则是多线程的。多线程必须要考虑的一个问题是线程之间的协作,协作即协调合作,不要乱套,以生产者和消费者模型而言,就是当缓冲区里没有数据时消费者要等待,等待生产者生产数据,当缓冲区满的时候生产者要等待,等待消费者消耗掉一些数据空出位置好存放数据。
java中为了实现多线程之间的协助,需要用到几个特性:wait(),notify(),notifyAll(),synchronized,synchronized相当于操作系统里的临界区或者锁的概念,所谓临界区就是说一次只能有一个线程进去,其他想进入的线程必须等待,加了synchronized锁后,才能调用wait(),notify()和notifyAll()操作,wait方法被调用后,当前线程A(举例)进入被加锁对象的线程休息室,然后释放锁,等待被唤醒。释放的锁谁来获取?当然是由先前等待的另一个线程B得到,B在获得锁后,进行某种操作后通过notify或者notifyAll把A从线程休息室唤醒,然后释放锁,A被唤醒后,重新获取锁定,进行下一语句的执行。
再回到生产者和消费者模型,如果引入了缓冲区的话就需要处理生产者线程和消费者线程之间的协作,缓冲区可以有这几种,队列缓冲区,比如队列或者栈,队列缓冲区的特点是其长度是动态增长的,这就意味着内存的动态分配带来的性能开销,同时队列缓冲区还会产生因为多线程之间的同步和互斥带来的开销。环形缓冲区可以解决内存分配带来开销的问题,因为环形缓冲区长度是固定的。但是环形缓冲区还是无法解决同步互斥带来的多线程切换的开销,如果生产者和消费者都不止一个线程,带来的开销更大,终极解决办法是引入双缓冲区,何为双缓冲区?双缓冲区顾名思义是有两个长度固定的缓冲区A B,生产者和消费者只使用其中一个,当两个缓冲区都操作完成后完成一次切换,开始时生产者开始向A里写数据,消费者从B里读取数据,当A写满同时B也读完后,切换一下,这时消费者从A里取数据,生产者向B写数据,由于生产者和消费者不会同时操作同一个缓冲区,所以不会发生冲突。
生产者和消费者模型不止是用在多线程之间,不同进程之间也可以有。线程和进程到底有什么区别?这是很多程序员搞不清的问题,其实很简单,进程有自己的地址空间和上下文,线程是在一个进程上并发执行的代码段。其实在win32系统中进程只是占用一定长度的地址空间,进程中总是有一个主线程来运行。消费者和生产者模型应用于进程间通信的典型例子是分布式消息处理,消息的消费者进程需要一个缓冲区缓冲收到的消息,消息的生产者进程也需要一个缓冲区缓冲将要发送的消息,这样可以一定程度上减少因为网络断开引起的消息丢失。
对于此模型,应该明确一下几点:
- 生产者仅仅在仓储未满时生产,仓满则停止生产。
- 消费者仅仅在仓储有产品时才能消费,仓空则等待。
- 当消费者发现仓储没有产品的时候会通知生产者生产。
- 生产者在生产出可消费产品的时候,应该通知等待的消费者去消费。
以下是它的具体实现:
1, public class ProducerConsumer {
2, public static void main(String []args) {
3, SyncStack ss=new SyncStack();
4, Producer p=new Producer(ss);
- Consumer c=new Consumer(ss);
- new Thread(p).start();
- new Thread(c).start();
- }
- }
- class WoTou {
- int id;
- WoTou(int id) {
- this.id=id;
- }
- public String toString() {
- return "WoTou : "+id;
- }
- }
- class SyncStack {
- int index=0;
- WoTou[] arrWT=new WoTou[6];
- public synchronized void push(WoTou wt) {
- while(index==arrWT.length) {
- try{
- this.wait();
- }catch(InterruptedException e) {
- e.printStackTrace();
- }
- }
- this.notify();
- arrWT[index]=wt;
- index++;
- }
- public synchronized WoTou pop() {
- while(index==0) {
- try{
- this.wait();
- }catch(InterruptedException e) {
- e.printStackTrace();
- }
- }
- this.notify(); //唤醒线程
- index--;
- return arrWT[index];
- }
- }
- class Producer implements Runnable {
- SyncStack ss=null;
- Producer(SyncStack ss) {
- this.ss=ss;
- }
- public void run() {
- for(int i=0;i<20;i++) {
- WoTou wt=new WoTou(i);
- ss.push(wt);
- System.out.println("生产了:"+wt);
- try{
- Thread.sleep((int)(Math.random()*2));
- }catch(InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
- class Consumer implements Runnable {
- SyncStack ss=null;
- Consumer(SyncStack ss) {
- this.ss=ss;
- }
- public void run() {
- for(int i=0;i<20;i++) {
- WoTou wt=ss.pop();
- System.out.println("消费了:"+wt);
- try{
- Thread.sleep((int)(Math.random()*1000));
- }catch(InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
附:1程序、进程和线程
程序,就是一段静态的可执行的代码。
进程,就是程序的一次动态的执行过程。
线程,是程序从头到尾的执行路线,也称为轻量级的进程。一个进程在执行过程中,可以产生多个线程,形成多个执行路线。但线程间是彼此相互独立的。各个线程可以共享相同的内存空间,并利用共享内存来完成数据交换、实时通信和一些同步的工作。而进程都占有不同的内存空间。
单线程是指一个程序只有一条从开始到结束的顺序的执行路线。
多线程是多个彼此独立的线程,多条执行路线。
2.wait()、notify()可以在任何位置调用,suspend()、resume()只能在synchronized()方法或代码块中调用。
3线程同步
当多个用户线程在并发运行中,可能会因为同时访问一些内容而产生错误问题。例如,同一时刻,一个线程在读取数据,另外一个线程在处理数据,当处理数据的线程没有等到读取数据的线程读取完毕就去处理数据,必然得到错误的结果。