Producer-Consumer Pattern

来源:互联网 发布:macbook怎样下载软件 编辑:程序博客网 时间:2024/06/05 02:00

什么是Producer-Consumer Pattern?

设想一个场景,生产者需要将数据安全地交给消费者,然而生产者和消费者运行在不同的线程上,两者的处理速度差将是最大的问题。消费者想要取出数据的时候生产者还没有建立数据,或者生产者建立数据的时候消费者还没办法接受数据等等。

这个模型就是在他们中间加入一个桥梁,处理线程之间的处理速度差。

当两方都只有一个的时候,我们也称之为Pipe Pattern(管道模型)

同样的,我们还是用代码来做一个示例,设想现在有一个场景,有一个桌子,桌子上最多能放3块蛋糕,3个厨师往桌子上制作蛋糕,3个食客吃桌子上的蛋糕,当桌子上的蛋糕个数等于3个的时候,厨师就不能再放蛋糕了,当桌子上的蛋糕等于0个的时候,食客就不能吃蛋糕了。我们来实现一下:

首先写一个table类:

package producerConsumerPattern;public class Table {    /*     * 原理上是循环队列     * */    private final String[] buffer;//存放蛋糕    private int tail;//尾巴    private int head;//头部    private int count;//buffer的蛋糕个数    public Table(int count){        this.buffer = new String[count];//最大能放的蛋糕个数        this.head = 0;        this.tail = 0;        this.count = 0;        //原理上是循环队列    }    //放置蛋糕 抛出的异常表示这个方法可被打断    public synchronized void put(String cake) throws InterruptedException{        while(count>=buffer.length){//警戒条件 当count<buffer.length的时候不能再放置            wait();        }        //进入临界区        buffer[tail] = cake;//放置在尾部        tail = (tail+1)%buffer.length;        count++;        System.out.println(Thread.currentThread().getName()+"puts "+cake+" ,还剩"+count+"块蛋糕");        notifyAll();//警戒条件可能发生了改变 对其他所有线程进行唤醒    }    //获取蛋糕    public synchronized String take() throws InterruptedException{        while(count<=0){//警戒条件 当count>0的时候才可以拿            wait();        }        String cake = buffer[head];//拿出头        head = (head + 1)%buffer.length;        count--;        System.out.println(Thread.currentThread().getName()+" takes "+cake+" ,还剩"+count+"块蛋糕");        notifyAll();//警戒条件可能发生了改变 对其他所有线程进行唤醒        return cake;    }}
然后写食客线程:

package producerConsumerPattern;import java.util.Random;public class EaterThread implements Runnable {    private final Table table;    private final Random random;    public EaterThread(Table table , long seed){        this.table = table;        this.random = new Random(seed);    }    @Override    public void run() {        try{            while(true){                table.take();                Thread.sleep(random.nextInt(1000));//模拟吃蛋糕的间隔            }        }        catch(InterruptedException e){                    }    }    }
实现厨师线程类:

package producerConsumerPattern;import java.util.Random;public class MakerThread implements Runnable {    /*     * 生产蛋糕的线程     * */    private final Random random;//随机产生生产蛋糕的时间    private final Table table;//放置的桌子    private static int id = 0;//蛋糕编号    public MakerThread(Table table,long seed){        this.table = table;        this.random = new Random(seed);    }    @Override    public void run() {        try{            while(true){                String cake = "No. "+nextId();                table.put(cake);                Thread.sleep(random.nextInt(1000));//随机休眠            }        }        catch(InterruptedException e){            e.printStackTrace();        }    }    private static synchronized int nextId(){        return id++;    }}
最后写测试类来进行测试:

package producerConsumerPattern;public class Test {    public static void main(String[] args) {        Table table = new Table(10);//可以放置3块蛋糕        EaterThread eaterThread = new EaterThread(table,System.currentTimeMillis());        MakerThread makerThread = new MakerThread(table,System.currentTimeMillis());        new Thread(eaterThread,"食客1").start();        new Thread(eaterThread,"食客2").start();        new Thread(eaterThread,"食客3").start();        new Thread(makerThread,"厨师1").start();        new Thread(makerThread,"厨师2").start();        new Thread(makerThread,"厨师3").start();    }}

运行结果如下:


现在我们回头看一下代码,实际上和我们之前说的guarded suspention pattern很类似,在Table这个类中,对于put方法和take方法都存在他自己的警戒条件,满足警戒条件就进入临界区,否则就进入wait set。我们在这两个方法都抛出了InterruptedException,说明这个方法是可以取消的方法。

关键在于保护安全性的table类,这个类控制了生产者消费者的共享互斥,关于synchronized、wait、notifyAll这些考虑多线程操作的代码,全都隐藏在Table类里,这也是这个模式的关键。


Producer-Consumer Pattern的所有参与者

1、data参与者

data参与者由producer参与者建立,由consumer参与者所使用

2、producer参与者

producer参与者建立data参与者,传递给channel参与者

3、consumer参与者

consumer参与者从channel参与者获取data参与者

4、channel参与者

channel参与者从producer参与者接收data参与者,并且保管起来。根据consumer的要求,将data参与者传送出去,为了确保安全,producer和consumer应该和访问进行共享排斥。(将访问方法放在channel对象里,用synchronized关键字进行修饰可以有效排斥)这个类对于生产者消费者的访问关系调控起到了极其重要的作用。

如果没有这个channel参与者,消费者处理的时间也在生产者生产的时间内,这很不合理(设想一下厨师做完直接送到客户嘴边,等客户吃完然后再做另一个蛋糕,这不合理)。


InterruptedException异常

在示例代码中,我们提到了InterruptedException,这通常表示着:

1、这是一个需要花点时间的方法

2、这是一个可以取消的方法

三个常用的抛出InterruptedException的方法有

1、java.lang.Object类的wait方法

花费时间:进入等待区需要被notify/notifyAll,在等待的期间,线程不会活动,因此需要花费时间。

取消操作:使用notify/notifyAll方法取消

2、java.lang.Thread类的sleep方法

花费时间:会暂停执行参数内设置的时间

取消操作:等待设置长度时间

3、java.lang.Thread类的join方法

花费时间:会等待到指定的线程结束为止,也会花费直到指定线程结束之前这段时间。

取消操作:等待指定线程结束


取消线程sleep()暂停状态的interrupt方法

现在存在一个线程a,a调用了sleep方法:Thread.sleep(9999999999);进行休眠着,对于另一个线程b,可以执行下面的语句a.interrupt();使得a放弃等待操作。

在执行interrupt方法的时候,不需要获取Thread实例的锁,任何线程在任何时刻都可以调用其他线程的interrupt方法。当sleep中的线程被调用interrupt方法会抛出InterruptedException异常,比如上方a线程抛出异常,这样a的控制权,就交给捕捉这个异常的catch块了


取消线程wait()等待状态的interrupt方法

当线程a在wait()等待的时候,也可以调用interrupt方法进行取消,表示不用等notify/notifyAll了,从等待区里直接出来,同样,a线程抛出InterruptedException异常。

但是当线程wait的时候,小心锁的问题,线程进入等待区的时候,会解除锁,当wait中的线程被调用interrupt的时候,会重新获取锁定,再抛出InterruptedException。获取锁之前,无法抛出这个异常。


notify方法与Interrupt方法

notify/notifyAll是Object类的方法,是该实例的等待区调用的,而不是对线程直接调用,notify/notifyAll方法所唤醒的线程会进入wait下一个语句。执行该方法需要获取锁。

interrupt方法是Thread的方法,对线程直接调用,当被interrupt的线程正在sleep或者wait的时候,会抛出InterruptedException异常。执行该方法不需要获取锁。


join方法和interrupt方法

当线程以join等待其他线程结束的时候,可以interrupt方法取消,和sleep时一样,会跳到catch模块。


Interrupt方法只是改变了中断状态

实际上这个方法只是改变了线程的中断状态(表示这个线程有没有被中断的状态)。不是一被调用interrupt方法就抛出InterruptedException。之所以有时候会抛出这个异常,是因为sleep、wait、join这些方法会不断检查中断状态的值,然后抛出InterruptedException,如果没有执行到这些方法,就不会抛出异常,而是一直进行自己的操作,直到执行到这些方法,才马上抛出InterruptedException。

总之,没有调用sleep、wait、join,那么InterruptedException时不会抛出的。



isInterrupted方法----检查中断状态

线程中断,返回true,没有中断,返回false。


Thread.interruptted方法----检查并且清除中断状态

检查当前线程是否中断,中断返回true并且设置为非中断状态,否则返回false。


interrupted方法和interrupt方法的区别:

interrupt方法将线程切换到中断状态

interrupted方法检查并且清除中断状态


不要使用Thread类的stop方法,很危险,因为就算线程在执行临界区间的内容,也会结束线程!

原创粉丝点击