生产者消费者问题

什么是生产者消费者问题?

生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多进程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个进程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有信号灯法等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。

代码实现

1、使用synchronized实现:

public class ProducerConsumer {
    public static void main(String[] args) {
        Commodity commodity = new Commodity();

        //创建一个生产者线程,生产10件商品
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    commodity.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"生产者1").start();

        //创建一个消费者线程,消费10件商品
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    commodity.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"消费者1").start();


    }
}

//商品类
class Commodity{
    //商品数量
    private int commodityNum = 0;

    //商品增加方法(相当于生产者)
    public synchronized void increment() throws InterruptedException {
        if(commodityNum != 0){//判断商品数量是否为0
            this.wait();//要是不为0,生产者等待
        }
        commodityNum++;//生产一个商品
        System.out.println(Thread.currentThread().getName()+"生产了一件商品,商品数量为"+commodityNum+"件");
        this.notifyAll();//生产完唤醒其他线程
    }
	
    //商品减少方法(相当于消费者)
    public synchronized void decrement() throws InterruptedException {
        if(commodityNum == 0){//判断商品数量是否为0
            this.wait();//要是为0,消费者等待
        }
        commodityNum--;//消费一个商品
        System.out.println(Thread.currentThread().getName()+"生产了一件商品,商品数量为"+commodityNum+"件");
        this.notifyAll();//消费完唤醒其他线程
    }
}

运行结果:
在这里插入图片描述

2、存在的问题

上面的程序看来好像没什么问题,生产者与消费者轮换执行,程序按照我们预想的执行完毕

但是,让我们多加入几个线程看看。

public class ProducerConsumer {
    public static void main(String[] args) {
        Commodity commodity = new Commodity();

        //创建一个生产者线程,生产10件商品
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    commodity.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"生产者1").start();

        //创建一个消费者线程,消费10件商品
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    commodity.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"消费者1").start();

        //创建一个生产者线程,生产10件商品
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    commodity.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"生产者2").start();

        //创建一个消费者线程,消费10件商品
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    commodity.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"消费者2").start();


    }
}

//商品类
class Commodity{
    //商品数量
    private int commodityNum = 0;

    //商品增加方法(相当于生产者)
    public synchronized void increment() throws InterruptedException {
        if(commodityNum != 0){//判断商品数量是否为0
            this.wait();//要是不为0,生产者等待
        }
        commodityNum++;//生产一个商品
        System.out.println(Thread.currentThread().getName()+"生产了一件商品,商品数量为"+commodityNum+"件");
        this.notifyAll();//生产完唤醒其他线程
    }
	
    //商品减少方法(相当于消费者)
    public synchronized void decrement() throws InterruptedException {
        if(commodityNum == 0){//判断商品数量是否为0
            this.wait();//要是为0,消费者等待
        }
        commodityNum--;//消费一个商品
        System.out.println(Thread.currentThread().getName()+"生产了一件商品,商品数量为"+commodityNum+"件");
        this.notifyAll();//消费完唤醒其他线程
    }
}

运行结果:
在这里插入图片描述

预想中在商品数量不等于0的情况下生产者线程应该等待

但是,可以看到在多个生产者消费者线程的情况下出现了商品数量不等于0生产者线程还在生产商品的情况,这就是我们要说的问题虚假唤醒

在还有商品的情况下,生产者线程1进入判断,判断完后线程等待,此时另一条生产者线程2拿到执行权同样进入判断然后等待,此时要是消费者线程消费一个商品后唤醒其他线程,生产者线程1与生产者线程2都会从判断内开始执行,不会再进行判断,两个线程同时生产商品,就会造成商品数量大于1,这就是虚假唤醒

3、解决方案

将if判断改为while循环即可,其他不用变化

这样下一次线程在while循环内被唤醒还是要判断商品数量,即可避免虚假唤醒问题

4、condition简单介绍

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。

Condition是个接口,基本的方法就是await()和signal()方法;Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition();调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock. Lock()和lock.unlock之间才可以使用

  • Condition中的await()对应Object的wait();
  • Condition中的signal()对应Object的notify();
  • Condition中的signalAll()对应Object的notifyAll()。

img

5、使用lock实现:

public class ProducerConsumer {
    public static void main(String[] args) {
        Commodity commodity = new Commodity();

        //创建一个生产者线程,生产10件商品
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    commodity.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"生产者1").start();

        //创建一个消费者线程,消费10件商品
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    commodity.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"消费者1").start();

        //创建一个生产者线程,生产10件商品
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    commodity.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"生产者2").start();

        //创建一个消费者线程,消费10件商品
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    commodity.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"消费者2").start();


    }
}

//商品类
class Commodity{
    //商品数量
    private int commodityNum = 0;

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    //condition.await(); // 等待
    //condition.signalAll(); // 唤醒全部

    //商品增加方法(相当于生产者)
    public  void increment() throws InterruptedException {
        lock.lock();
        try {
            while (commodityNum != 0){//判断商品数量是否为0
                condition.await();//要是不为0,生产者等待
            }
            commodityNum++;//生产一个商品
            System.out.println(Thread.currentThread().getName()+"生产了一件商品,商品数量为"+commodityNum+"件");
            condition.signalAll();//生产完唤醒其他线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    
	//商品减少方法(相当于消费者)
    public  void decrement() throws InterruptedException {
        lock.lock();
        try {
            while (commodityNum == 0){//判断商品数量是否为0
                condition.await();//要是为0,消费者等待
            }
            commodityNum--;//消费一个商品
            System.out.println(Thread.currentThread().getName()+"生产了一件商品,商品数量为"+commodityNum+"件");
            condition.signalAll();//消费完唤醒其他线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();//解锁
        }
    }
}

参考:

【狂神说Java】JUC并发编程最新版通俗易懂_哔哩哔哩_bilibili

Java并发之Condition - 数月亮 - 博客园 (cnblogs.com)

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值