Java并发中生产者消费者模式

版权声明:转载请注明原址,有错误欢迎指出 https://blog.csdn.net/iwts_24/article/details/86795714

生产者消费者模式

        很简单的例子,KFC吃饭,柜台前顾客排队点餐,假设KFC只卖汉堡,那么就是这样的情景:消费者排队花钱下单购买,生产者(厨师)在后厨疯狂做汉堡。如果只有一个厨师,柜台只有一个服务员,那么就类似一对一的情况:消费者可能有多个,但是需要好好排队,厨师做一个消费者拿一个。如果厨师正在做,消费者就只能等待——还是在排队的,不能插队。如果没人光顾,厨师就闲着。这就是单线程。

        如果考虑多线程并发,就是好几个厨师和好几个服务员,消费者可以从多个窗口付费等餐。但是整体上仍然可能需要等待。如果有保温箱,厨师可以做汉堡放进保温箱里,这样顾客来的时候可以直接掏钱走人。这是多线程下的生产者消费者模式。

        抽象一点:可以开多条线程,共享资源为商品数量。生产者生产商品,消费者消费商品。为了保证安全,我们需要对商品数量加同步锁,那么下面就是比较简单的生产者消费者的实现。

工厂类

        这个工厂并不是工厂模式,有点类似上面例子中的KFC,因为我们需要用同步锁来保证商品的安全性,所以在工厂中定义操作方法对商品进行加同步锁,而通过依赖注入注入到具体的生产者类和消费者类中。

package me.iwts.test;

public class Factory{
    private static final int MAX_NUM = 100;
    private static int number;

    public Factory(){
        number = 0;
    }

    public synchronized void produce(){
        while (number > MAX_NUM){
            try {
                System.out.println("产品足够,不需要生产");
                wait();
            }catch (Exception ex){
                ex.printStackTrace();
            }
        }

        number++;
        System.out.println("第"+number+"个产品已被生产");
        notifyAll();
    }

    public synchronized void consume(){
        while(number <= 0){
            try{
                System.out.println("商品售罄,无法售出");
                wait();
            }catch (Exception ex){
                ex.printStackTrace();
            }
        }

        System.out.println("第"+number+"个商品已售出");
        number--;
        notifyAll();
    }
}

synchronized锁方法,而将Factory实例对象作为监听器,这样就能够保证number变量的安全。那么在while循环中,判定商品足够或者商品不够的时候,调用wait()进入阻塞队列,防止继续生产或者购买,而另一个方法在生产或者售出一个商品之后,调用notify()解锁。

        其实这就是生产者消费者的核心了,最简单的实现其实就这样,然后写个run()方法就可以new线程了。但是为了稍微符合一点实际写代码的方法,博主用依赖注入的方法,解耦消费者和生产者。

生产者类

package me.iwts.test;

public class Produce implements Runnable {
    public Factory factory;

    public Produce(Factory factory){
        this.factory = factory;
    }

    @Override
    public void run() {
        int i = 100;
        while (i-- != 0){
            try{
                Thread.sleep(30);
            }catch (Exception ex){
                ex.printStackTrace();
            }
            factory.produce();
        }
    }
}

构造器注入,注册进工厂,就好比KFC加入了一个厨师,而其run()方法保证了在开启这个厨师的线程之后,可以总共干100次活,每次间隔30ms(调用sleep()方法睡30ms),干活就是调用注入的工厂的priduce()方法:factory.produce()。

消费者类

package me.iwts.test;

public class Consume implements Runnable {
    public Factory factory;

    public Consume(Factory factory){
        this.factory = factory;
    }

    @Override
    public void run() {
        int i = 20;
        while(i-- != 0){
            try {
                Thread.sleep(130);
            }catch (Exception ex){
                ex.printStackTrace();
            }
            factory.consume();
        }
    }
}

消费者其实不算是我们认为的顾客,而是理解为一个前台服务员,或者说前台付款窗口。while循环20次,代表有20个顾客来购买,每个顾客到来间隔了130ms(sleep()方法睡130ms)。而每次顾客购买就是调用工厂的consume()方法。

并发

        比较简单的情况,就是new出来生产者和消费者之后,作为线程开启。就可以模拟了,给一个main()方法:

public class Main{
    public static void main(String[] args){
        Factory factory = new Factory();
        Produce produce = new Produce(factory);
        Consume consume = new Consume(factory);

        new Thread(produce).start();
        new Thread(consume).start();
    }
}

这样,就等于说我们开了一个简单的KFC,或者说手推车版的KFC。有一个厨师,只有一个付费窗口。厨师每隔30ms就能做一个汉堡,顾客每隔130ms就来一个购买。顾客和厨师都是单独的线程,所以都在工作。

        这样看,其实都是厨师在疯狂工作——因为只要汉堡不够100个就得做,而顾客少来的慢,每次来就拿着汉堡走。所以实际上是不用排队的。可以看一下输出的一部分:

第1个产品已被生产
第2个产品已被生产
第3个产品已被生产
第3个商品已售出
第3个产品已被生产
第4个产品已被生产
第5个产品已被生产
第5个商品已售出

太冷清了,改一下睡觉的时间,让顾客排排队,那么就是修改sleep()的时间,调换一下顺序吧,厨师每隔130ms做一个汉堡,而顾客30ms来一个。

商品售罄,无法售出
第1个产品已被生产
第1个商品已售出
商品售罄,无法售出
第1个产品已被生产
第1个商品已售出
商品售罄,无法售出
第1个产品已被生产
第1个商品已售出

这里就可以看到我们加同步锁的好处了,保证了安全。而此时就出现了阻塞队列,实际上有多个顾客在等待,可以看到,每做出来一个就被直接购买,然后就出现售罄显示。但是毕竟总共只有20个顾客,而商人要做100个汉堡。所以这样的情况重复20次后就只剩一致增加的情况了:

商品售罄,无法售出
第1个产品已被生产
第1个商品已售出
第1个产品已被生产
第2个产品已被生产
第3个产品已被生产
第4个产品已被生产 

扩大生产规模

        我们的KFC要做大做强!所以扩展多个厨师和多个服务员,实际上就是new出来多个Produce类和Consume类。算是真正的多线程并发工作了。为了方便,我们需要为厨师和服务员命名,以Produce为例,改成这样:

package me.iwts.test;

public class Produce implements Runnable {
    public Factory factory;
    public String name;

    public Produce(Factory factory,String name){
        this.factory = factory;
        this.name = name;
    }

    @Override
    public void run() {
        int i = 100;
        while (i-- != 0){
            try{
                Thread.sleep(130);
            }catch (Exception ex){
                ex.printStackTrace();
            }
            factory.produce(name);
        }
    }
}

其实就是多了个名字,显示上好看一点,方便我们区分线程。而传入了了具体的方法,在输出的时候前面加上name就行了。最后输出如下:

服务员2号 说:商品售罄,无法售出
服务员1号 说:商品售罄,无法售出
厨师1号 说:第1个产品已被生产
服务员1号 说:第1个商品已售出
服务员2号 说:商品售罄,无法售出
厨师2号 说:第1个产品已被生产
服务员2号 说:第1个商品已售出
服务员1号 说:商品售罄,无法售出
服务员2号 说:商品售罄,无法售出
厨师2号 说:第1个产品已被生产
服务员2号 说:第1个商品已售出
服务员1号 说:商品售罄,无法售出

看起来是扩大规模了。最终仍然是因为顾客太少,会开始制作汉堡但是没人买:

厨师1号 说:第1个产品已被生产
服务员1号 说:第1个商品已售出
厨师2号 说:第1个产品已被生产
厨师1号 说:第2个产品已被生产
厨师1号 说:第3个产品已被生产
厨师2号 说:第4个产品已被生产
厨师2号 说:第5个产品已被生产 

main方法就是多开几个线程罢了,看一下代码:

public class Main{
    public static void main(String[] args){
        Factory factory = new Factory();
        Produce produce1 = new Produce(factory,"厨师1号");
        Produce produce2 = new Produce(factory,"厨师2号");
        Consume consume1 = new Consume(factory,"服务员1号");
        Consume consume2 = new Consume(factory,"服务员2号");

        new Thread(produce1).start();
        new Thread(produce2).start();
        new Thread(consume1).start();
        new Thread(consume2).start();
    }
}

猜你喜欢

转载自blog.csdn.net/iwts_24/article/details/86795714