逐步剖析生产者消费者问题-Java多线程经典案例

0 导言

在Java多线程中有一个经典案例,生产者和消费者(两个线程)对商品(共有对象)进行操作。在本文中,笔者将逐步实现案例,从功能雏形开始逐步完善功能,以保证对Java多线程的同步锁等功能进行深入了解。

在本文中,你将了解Java多线程的以下内容:

  1. Synchronized关键字  ->  同步锁
  2. wait 和 notify 方法
  3. Flag标识符

本案例:

生产者生产商品(赋予商品对象Brand和Name);

消费者购买商品(取走商品的Brand和Name);


1 原始版本

至此,我们完成了对需求的设定,我们可以大致分为以下四类:

  1. 商品类,包含Brand和Name的属性
  2. 生产者类,赋予共享的商品类对象以Brand和Name;
  3. 消费者类,取走商品的Brand和Name;
  4. 主函数,用于实现程序运行。

 基于以上设计方案,我们分别对其进行实现。

1.1 Goods类

//Goods类
public class Goods {

    private String brand;
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }
}

1.2 生产者类

/*
* 生产者任务:
*       生产产品,存入共享空间中。
* */
public class Producer implements Runnable {

    private Goods goods;

    public Producer(){

    }

    public Producer(Goods goods) {
        this.goods = goods;
    }

    @Override
    public void run() {
        for( int i = 0 ; i < 10 ; i++){
            if ( i % 2 == 0 ){
                goods.setBrand("哇哈哈");
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                goods.setName("矿泉水");
            }else{
                goods.setBrand("旺仔");
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                goods.setName("小馒头");
            }
            System.out.println("生产了" + this.goods.getBrand() + this.goods.getName());
        }
    }
}

1.3 消费者类

/*
* 消费者任务:
*       从共享空间中取走商品
* */
public class Customer implements Runnable{

    private Goods goods;

    public Customer(){

    }

    public Customer(Goods goods) {
        this.goods = goods;
    }

    @Override
    public void run() {
        for( int i = 0 ; i < 10 ; i ++){
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("消费者取走了" +
                    this.goods.getBrand() + "-----" + this.goods.getName());
        }
    }
}

1.4主函数

/*
* 实现功能:
*       消费者和生产者
*       前者可以从一块区域取走商品
*       后者可以生产商品放入区域
* */
public class Test {

    public static void main(String[] args) {

        Goods goods = new Goods();

        Producer producer = new Producer(goods);
        Customer customer = new Customer(goods);

        Thread t1 = new Thread(producer);
        Thread t2 = new Thread(customer);
        t1.start();
        t2.start();
    }
}

1.5 输出结果

完成实现后,我们会获得输出结果如下(节选):

生产了哇哈哈矿泉水
消费者取走了哇哈哈-----矿泉水
生产了旺仔小馒头
消费者取走了旺仔-----矿泉水
消费者取走了哇哈哈-----小馒头
生产了哇哈哈矿泉水
生产了旺仔小馒头
消费者取走了旺仔-----小馒头

多尝试几次,我们会遇到以下问题:

  1. 生产和消费的商品的属性不一致(本质上是生产者存储属性到一半的时候,被消费者取走)
  2. 生产和消费乱序,生产了一个商品却被多次取走

2 解决方案

2.1 问题一:生产和消费的商品的属性不一致

对此,我们想到了同步锁,Synchronized关键字

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种: 

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象; 
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象; 
  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象; 
  4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

源自:Java中Synchronized的用法(简单介绍)

简而言之,被Synchronized关键字修饰的方法,在被一个线程调用的时候,其他线程无法对其进行访问和调用。

这样,我们就可以保证消费者只能在生产者调用结束之后,才能进行消费(即生产者完成商品所有属性的存储,消费者才能取走),这样,消费者就只能在生产者结束生产(完成数据的完整存储之后)才能购买(读取商品的属性),不会再出现商品名不对应的问题。

2.2 问题二:生产和消费乱序,生产了一个商品却被多次取走

对于这个问题,我们首先想到的就是设立一个标识位Flag,用于标识商品是否存在。

  1. 默认,我们将Flag设为false,即商品不存在。
  2. 生产者只能在商品不存在的时候生产,并将商品标识符设为true(即已补充货物)
  3. 消费者只能在商品存在的时候消费,并将商品标识符设为false(即已被买走)

但这样,我们会发现一个新的问题:

由于生产者和消费者访问时间不定,所以很可能出现:

  • 消费者买东西的时候,出现没商品的情况,停止购买;
  • 生产者生产的时候,发现商品存在,停止生产;

即最终打印的,往往不是20条语句。

那么这就引起我们思考-----------------------------------

当生产者发现商品存在的时候,应该等待商品售完,再进行补货,而不是直接离开店里。

(不应该结束当前方法,应该处于等待状态,当标识符为false的时候,继续访问)

为了实现这一点,就得利用wait和notify方法。

查看源码,我们会发现,这两点不是针对的线程类,而是存在于Object类中,即所有类的父类。

那么让我们来了解以下他们的原理:

void notify() 
Wakes up a single thread that is waiting on this object’s monitor. 
译:唤醒在此对象监视器上等待的单个线程

void notifyAll() 
Wakes up all threads that are waiting on this object’s monitor. 
译:唤醒在此对象监视器上等待的所有线程

void wait( ) 
Causes the current thread to wait until another thread invokes the notify() method or the notifyAll( ) method for this object. 
译:导致当前的线程等待,直到其他线程调用此对象的notify( ) 方法或 notifyAll( ) 方法

void wait(long timeout) 
Causes the current thread to wait until either another thread invokes the notify( ) method or the notifyAll( ) method for this object, or a specified amount of time has elapsed. 
译:导致当前的线程等待,直到其他线程调用此对象的notify() 方法或 notifyAll() 方法,或者指定的时间过完。

void wait(long timeout, int nanos) 
Causes the current thread to wait until another thread invokes the notify( ) method or the notifyAll( ) method for this object, or some other thread interrupts the current thread, or a certain amount of real time has elapsed. 
译:导致当前的线程等待,直到其他线程调用此对象的notify( ) 方法或 notifyAll( ) 方法,或者其他线程打断了当前线程,或者指定的时间过完。

来源:wait和notify的理解与使用

以生产者示例,即:

我们可以在发现商品存在的时候,让线程进行等待。(wait)

直至消费者买走商品,唤醒生产者继续消费。(notify)

至此,我们完成所有的代码,并实现了完整的生产与消费流程。(撒花)

3 完整源码

附上源码如下:

3.1 Goods类

public class Goods {

    private String brand;
    private String name;
    //标识是否存在商品,默认为false
    private boolean flag = false;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }



    //消费者购买商品
    public synchronized void get() {
        if ( !this.flag){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("消费者取走了" +
                this.getBrand() + "-----" + this.getName());
        this.flag = false;
        notify();
    }


    //生产者生产商品
    public synchronized void set(String brand, String name) {
        if(this.flag) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        this.setBrand(brand);
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        this.setName(name);
        System.out.println("生产者生产了" + this.getBrand() +
                "----" + this.getName());
        this.flag = true;
        notify();
    }


}

3.2 生产者类

public class Producer implements Runnable {

    private Goods goods;

    public Producer(){

    }

    public Producer(Goods goods) {
        this.goods = goods;
    }

    @Override
    public void run() {
        for( int i = 0 ; i < 9 ; i++){
            if( i % 2 == 0){
                goods.set("哇哈哈","矿泉水");
            } else {
                goods.set("旺仔","小馒头");
            }
        }
    }
}

3.3 消费者类

public class Customer implements Runnable{

    private Goods goods;

    public Customer(){

    }

    public Customer(Goods goods) {
        this.goods = goods;
    }

    @Override
    public void run() {
        for( int i = 0 ; i < 9 ; i++){
            goods.get();
        }
    }
}

3.4 主函数类

public class Test {

    public static void main(String[] args) {

        Goods goods = new Goods();

        Producer producer = new Producer(goods);
        Customer customer = new Customer(goods);

        Thread t1 = new Thread(producer);
        Thread t2 = new Thread(customer);
        t1.start();
        t2.start();
    }
}

强烈推荐各位学习的小伙伴认真看看本文中附上的两篇博客。

当然,你也可以选择免费下载本文逐步实现的源码:

 生产者与消费者问题学习源码-互联网文档类资源-CSDN文库

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我想脱离小码农

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值