Producer-Consumer Pattern

什么是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方法,很危险,因为就算线程在执行临界区间的内容,也会结束线程!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值