线程间通信——生产者和消费者模式

消费者和生产者模式

是用来描述一个仓库(缓冲区),生产者可以将产品放入到仓库中,消费者可以从仓库中买走商品,解决生产者和消费者的生活逻辑问题,需要采用代码的同步机制来完成相互的【约束】和【提醒】。

注意事项

  1. 【商品是唯一共享资源】
  2. 消费者购买商品,清空商品的库存,要【提醒】生产者生产,并且消费者停止购买
  3. 生产者生产商品,填满商品的库存,要【提醒】消费者购买(到货通知),并且生产者是要停止生产操作

在这里插入图片描述

实现方法

采用wait()、notify()和notifyAll()方法。

wait():当缓冲区已满或空时,生产者/消费者线程停止自己的执行,放弃锁,使自己处于等待状态,让其他线程执行

  • 是Object的方法
  • 调用方式:对象.wait();
  • 表示释放 对象 这个锁标记,然后在锁外边等待(对比sleep(),sleep是抱着锁休眠的)
  • 等待,必须放到同步代码段中执行

notify():当生产者/消费者向缓冲区放入/取出一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态

  • 是Object的方法
  • 调用方式:对象.notify();
  • 表示唤醒 对象 所标记外边在等待的一个线程,或者从多个待唤醒线程中随机唤醒一个

notifyAll():全部唤醒

  • 是Object的方法
  • 调用方式:对象.notifyAll()
  • 表示唤醒 对象 所标记外边等待的所有线程

案例

生产者生产面包,消费者消费面包,要求仓库有面包时,提醒消费者消费,生产者出于等待状态;当没有面包的时候,提醒生产者生产,消费者处于等待。即生产一件,消费一件。
基本思路:

  1. 创建面包类,构建相关的属性和方法。
  2. 创建面包仓库类,面包类对象传入,并创建sychronized修饰的同步方法——input和output方法。
  3. 创建两个线程,生产者product和消费者consumer,分别继承Runnable,重写run()方法,并分别调用input和output方法,保证同步。
  4. 在主方法创建Thread,并start。
/**
* 面包类
*/
public class Bread {
    private int id;
    private String productName;

    public Bread() {
    }
    public Bread(int id, String productName) {
        this.id = id;
        this.productName = productName;
    }

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getProductName() {
        return productName;
    }
    public void setProductName(String productName) {
        this.productName = productName;
    }
}

/**
* 面包容器类
*/
public class BreadCon {
    private Bread con;
    private boolean flag = false;   //判断仓库是否有面包,false表示没有

    public synchronized void input(Bread b) throws InterruptedException {
        if (flag) {
            this.wait();	//如果仓库有面包,则处于阻塞状态,等待消费者消费后唤醒
        }
        this.con = b;
        System.out.println(Thread.currentThread().getName() + "生产了" 
        + b.getId() + "号面包");
        flag = true;
        this.notify();	//仓库没有面包,则生产面包,并将标记改为true,唤醒消费者购买
    }

    public synchronized void output() throws InterruptedException {
        if (!flag) {
            this.wait();
        }
        Bread b= con;  
        con = null;
        System.out.println(Thread.currentThread().getName()+"消费了"
        +b.getId() + "号面包" +" 生产者:"+b.getProductName());
        flag = false;	//修改标记
        this.notify();	//唤醒生产者生产
    }
}

/**
* 生产者类
*/
public class Product implements Runnable{
    private BreadCon con;
    public Product(BreadCon con) {
        this.con = con;
    }
    @Override
    public void run() {
        for (int i = 1; i <= 30; i++) {
            Bread b = new Bread(i, Thread.currentThread().getName());
            try {
                this.con.input(b);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
* 消费者类
*/
public class Consumer implements Runnable {
    private BreadCon con;
    public Consumer(BreadCon con) {
        this.con = con;
    }
    @Override
    public void run() {
        for (int i = 1; i <= 30; i++) {
            try {
                con.output();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
* 测试类
*/
public class test {
    public static void main(String[] args) {
        //创建容器
        BreadCon con = new BreadCon();
        //生产
        Product product = new Product(con);
        //消费
        Consumer consumer = new Consumer(con);
        //线程对象启动
        new Thread(product, "面包生产商A").start();
        new Thread(consumer, "消费者1").start();
    }
}

//打印结果
面包生产商A生产了1号面包
消费者1消费了1号面包 生产者:面包生产商A
面包生产商A生产了2号面包
消费者1消费了2号面包 生产者:面包生产商A
面包生产商A生产了3号面包
消费者1消费了3号面包 生产者:面包生产商A
面包生产商A生产了4号面包
消费者1消费了4号面包 生产者:面包生产商A
面包生产商A生产了5号面包

思考:如果生产方不止一个,消费方也不止一个,如何设计?

我们分别为生产者和消费者创建两个线程,运行结果如下:

new Thread(product, "面包生产商A").start();
new Thread(product, "面包生产商B").start();
new Thread(consumer, "消费者1").start();
new Thread(consumer, "消费者2").start();

//打印结果
//不止运行结果异常,而且还发生了死锁
面包生产商A生产了1号面包
消费者1消费了1号面包 生产者:面包生产商A
消费者2消费了1号面包 生产者:面包生产商A
面包生产商A生产了2号面包
消费者1消费了2号面包 生产者:面包生产商A
消费者2消费了2号面包 生产者:面包生产商A

为何会发生错误和死锁?

  1. 生产商A抢到CPU,执行生产面包,修改标记为true
  2. 生产商A抢到CPU,但是标记为true,等待,释放CPU和锁
  3. 生产商B抢到CPU,但是标记为true,等待,释放CPU和锁
  4. 消费者1抢到CPU,执行消费面包,修改标记为false,唤醒生产商A
  5. 生产商A抢到CPU,执行生产面包,修改标记为true,唤醒生产商B(notify随机唤醒一个)
  6. 生产商B抢到CPU,但是标记为true,等待,释放CPU和锁
  7. 生产商A抢到CPU,但是标记为true,等待,释放CPU和锁
  8. 消费者1抢到CPU,执行消费面包,修改标记为false,唤醒消费者2(notify随机唤醒一个)
  9. 消费者2抢到CPU,但是标记为false,等待,释放CPU和锁
  10. 消费者1抢到CPU,但是标记为false,等待,释放CPU和锁

最终四者全部处于阻塞状态,都在等待唤醒,发生死锁。

另外,当处于阻塞状态的线程被唤醒的时候,会继续执行后面代码。在第2、3步,A与B都被阻塞,在第4步,A被唤醒后,会继续执行后面生产面包的代码,然后在第5步,又抢到时间片,又会执行生产面包的代码,然后notify还唤醒了另一个生产商B,B会继续执行后面的生产面包代码,这样会出现多生产2次面包的情况,即错误状态。

解决办法:

  • 将 if 判断语句改为 while 循环判断语句,保证被唤醒后,不再继续执行生产面包代码,而是返回进行循环判断判断
  • 将 notify 改为 notifyAll ,避免发生死锁情况。
//这里只针对面包容器类进行修改
public class BreadCon {
    private Bread con;
    private boolean flag = false;   //判断仓库是否有面包,false表示没有

    public synchronized void input(Bread b) throws InterruptedException {
        while (flag) {
            this.wait();
        }
        this.con = b;
        System.out.println(Thread.currentThread().getName() + "生产了" + b.getId()
         + "号面包");
        flag = true;
        this.notifyAll();
    }
    public synchronized void output() throws InterruptedException {
        while (!flag) {
            this.wait();
        }

        /*Bread b= con;
        con = null;*/
        System.out.println(Thread.currentThread().getName()+"消费了"+con.getId() 
        + "号面包" +" 生产者:"+con.getProductName());
        flag = false;
        this.notifyAll();
    }
}

//打印结果
面包生产商A生产了1号面包
消费者1消费了1号面包 生产者:面包生产商A
面包生产商B生产了1号面包
消费者2消费了1号面包 生产者:面包生产商B
面包生产商A生产了2号面包
消费者1消费了2号面包 生产者:面包生产商A
面包生产商B生产了2号面包
消费者2消费了2号面包 生产者:面包生产商B

效率问题
虽然解决了错误和死锁问题,但如果生产者和消费者数量及其庞大,每次notifyAll会将无关的线程也唤醒,增加了无效的判断时间,效率大打折扣,所以这里采用Condition接口,配合Lock接口下的ReentrantLock实现类解决效率问题。

Condition接口

sychronized的锁方式,只能让生产者和消费者进入同一个阻塞队列,所以notifyAll会将队列中所有线程唤醒。

Condition接口则提供了两个队列,一个消费队列,一个生产队列,进行锁的微操,甚至可以自定义提供三个甚至更多队列,实现多线程的同步,并保证效率。

方法的使用

//创建Lock对象
Lock lock = new ReentrantLock();

//根据需求创建自定义数量的Condition条件队列
Condition condition = lock.newCondition();

//使某一队列等待,相当于wait()
condition.await();

//使某一队列唤醒,相当于notify()
condition.signal(); 
//

案例一:面包案例优化

这次我们将面包放到一个数组进行生产,当数组满了以后可以提醒消费者购买,所以也会在没满的时候,消费者也会购买,模拟实际的生产和消费模式。

//这里只提供核心代码块
public class BreadCon {
    /*这里必须用-1,否则出现索引越界异常,因为后面消费者消费index--会出现index=-1的情况
     *为了避免因为生产者生产的时候breads[index]调取异常
     *会将index++放到赋值之前,保证索引从0开始
     */
    private int index = -1;
    private Bread[] breads = new Bread[5];

    //创建ReentrantLock锁对象,并创建两个阻塞队列,一个是生产者proCon,一个是消费者conCon
    private Lock lock = new ReentrantLock();
    private Condition proCon = lock.newCondition();
    private Condition conCon = lock.newCondition();

    public BreadCon() {
    }

    public void input(Bread bread) {
        lock.lock();    //上锁

        try {
            while (index > 3) {
                proCon.await(); //如果面包数量超过数组容量,则生产者处于生产阻塞队列
            }
            index++;    //因为Index是从-1开始,所以放到赋值之前,从0开始索引
            breads[index] = bread;
            System.out.println("生产者:" + Thread.currentThread().getName() 
            + "生产了" + bread.getId() + "号面包");
            conCon.signal();    //如果有面包,则将消费队列消费者唤醒
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); //解锁
        }
    }

    public void output()  {
        lock.lock();

        try {
            while (index < 0) {
                try {
                    conCon.await();     //如果没有面包,则将消费者置于消费阻塞队列
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            Bread b = breads[index];
            breads[index] = null;
            System.out.println("消费者:" + Thread.currentThread().getName() 
            + "消费了" + b.getId() + "号面包,生产者是:" + b.getProductName());
            index--;
            proCon.signal();        //每消费一次面包,可以唤醒生产者生产
        } finally {
            lock.unlock();
        }
    }
}

案例二:三个线程交替输出A B C ,输出20遍

这里需要构筑三个Condition队列锁。

public class PrintABC {
    private int flag = 1;   //1表示A, 2表示B, 3表示C
    private Lock lock = new ReentrantLock();
    private Condition aCon = lock.newCondition();
    private Condition bCon = lock.newCondition();
    private Condition cCon = lock.newCondition();

    public void printA() {
        lock.lock();
        try {
            while (1 != flag) {
            	//这里注意,必须将阻塞放到循环判断中
            	//这样最后一次循环的时候,A被唤醒,可以继续执行下面的代码,依次唤醒BC,线程退出
            	//否则,如果将后面代码放到判断里,将里面的阻塞放到外面,则A被唤醒,却不能调用signal()方法,导致B/C一直被阻塞,main线程退出,但是子线程还在,发生死锁,后面循环也不会继续
                aCon.await();
            }
            Thread.sleep(200);
            System.out.println(Thread.currentThread().getName());
            bCon.signal();
            flag = 2;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB() {
        lock.lock();
        try {
            while (2 != flag) {
                bCon.await();
            }
            Thread.sleep(200);
            System.out.println(Thread.currentThread().getName());
            cCon.signal();
            flag = 3;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printC() {
        lock.lock();
        try {
            while (3 != flag) {
                cCon.await();
            }
            Thread.sleep(200);
            System.out.println(Thread.currentThread().getName());
            aCon.signal();
            flag = 1;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

//测试方法
public class test {
    public static void main(String[] args) {
        PrintABC printABC = new PrintABC();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    printABC.printA();
                }
            }
        }, "A").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    printABC.printB();
                }
            }
        }, "B").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    printABC.printC();
                }
            }
        }, "C").start();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值