4.生产者和消费者的问题(线程通信)

线程间的通信问题,用一个最经典的例子来讲就是生产者和消费者问题:

假设我们现在有两条线程P、C,它们操作着同一个变量num,每次当num=0时,P线程就对num进行+1操作,每次num=1时C就对num进行-1操作。这就好比一个商家生产东西,一个客户买东西,商家生产一个,客户就买一个。然而线程之间是相互隔离的,我们要让两条线程能够做到对同一个资源进行操作,就必须要进行线程通信。

public class ProductorAndCustomer {
    public static void main(String[] args) {
        //资源
        Data data = new Data();
        //生产者线程
        new Thread(()->{
            try {
                for (int i = 0; i < 10; i++) {
                    data.increment();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"Productor").start();
        //消费者线程
        new Thread(()->{
            try {
                for (int i = 0; i < 10; i++) {
                    data.decrement();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"Customer").start();
    }
}
//资源
class Data{
    private int num = 0;

    //生产
    public synchronized void increment() throws InterruptedException {
        if (num != 0){
            //说明生产的产品未被消费者消费,因此等待被消费
            this.wait();
        }
        //说明生产的产品被消费者消费了,因此再进行一次生产
        num++;
        System.out.println(Thread.currentThread().getName()+"->生产了产品:num = "+num);
        //生产完毕后需要通知消费者进行消费
        this.notify();
    }

    //消费
    public synchronized void decrement() throws InterruptedException {
        if (num == 0){
            //说明生产者未生产产品,因此等待生产者生产产品
            this.wait();
        }
        //说明生产已经生产了一个产品,因此再进行一次消费
        num--;
        System.out.println(Thread.currentThread().getName()+"->消费了产品:num = "+num);
        //消费完毕后需要通知生产者进行生产
        this.notify();
    }
}

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

可以看到,每当生产者进行一次生产,消费者就会进行一次对应的消费,如果消费者先进行消费,这时生产的产品为0,消费者就会进入等待状态(执行消费者的wait()方法 ),等待生产者进行生产,如果生产者生产了一个产品,又被执行了一次生产,那么这时产品数不为0,因此进入等待状态( 执行生产者的wait()方法 )等待消费者进行消费。

但是这样的生产者和消费者模式会存在一个问题:

一般来讲,我们的生产者和消费者可能会有多个。假设现在新来了一个消费者和生产者,我们再观察一下运行结果

//生产者线程1
new Thread(()->{
    try {
        for (int i = 0; i < 30; i++) {
            data.increment();
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
},"Productor1").start();
//生产者线程2
new Thread(()->{
    try {
        for (int i = 0; i < 30; i++) {
            data.increment();
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
},"Productor2").start();
//消费者线程1
new Thread(()->{
    try {
        for (int i = 0; i < 30; i++) {
            data.decrement();
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
},"Customer1").start();
//消费者线程2
new Thread(()->{
    try {
        for (int i = 0; i < 30; i++) {
            data.decrement();
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
},"Customer2").start();

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

可以看到,出现了问题:生产者多生产了几个产品。这就是多线程中的虚假唤醒问题。
我们查看一下官方api对这一块的解释:
在这里插入图片描述
所谓的虚假唤醒,就是用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if代码块之后的代码,而如果使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。

我们将资源类中的if替换成while再来进行尝试:

//生产
public synchronized void increment() throws InterruptedException {
    while (num != 0){
        //说明生产的产品未被消费者消费,因此等待被消费
        this.wait();
    }
    //说明生产的产品被消费者消费了,因此再进行一次生产
    num++;
    System.out.println(Thread.currentThread().getName()+"->生产了产品:num = "+num);
    //生产完毕后需要通知消费者进行消费
    this.notifyAll();
}

//消费
public synchronized void decrement() throws InterruptedException {
    while (num == 0){
        //说明生产者未生产产品,因此等待生产者生产产品
        this.wait();
    }
    //说明生产已经生产了一个产品,因此再进行一次消费
    num--;
    System.out.println(Thread.currentThread().getName()+"->消费了产品:num = "+num);
    //消费完毕后需要通知生产者进行生产
    this.notifyAll();
}

在这里插入图片描述

Lock(JUC)版的生产者消费者问题

我们在使用synchronized来描绘生产者和消费者问题时,使用的是wait()方法和notifyAll()方法来进行线程通信,在JUC中,我们有没有同样的方式呢?来看一下api吧:
在这里插入图片描述

可以看到,官方文档告诉我们使用锁对象来进行.newCondition()来创建一个条件对象(同步监视器),对应Object类中waitnotifyAll的作用,用来进行线程通信。
在这里插入图片描述
在JUC中,我们使用Condition实例中的.await()方法来进行线程等待,使用.signal()方法来进行线程唤醒。对应synchronized中的wait()notifyAll()方法。如下图:
在这里插入图片描述
代码实现:

//资源
class Data{
    private int num = 0;		//资源
    Lock lock = new ReentrantLock();	//创建锁
    Condition condition = lock.newCondition();	//通过锁对象来创建同步监视器
    //生产
    public void increment() throws InterruptedException {
        lock.lock();    //加锁
        try {
            //业务代码
            while (num != 0){
                //说明生产的产品未被消费者消费,因此等待被消费
                condition.await();
            }
            //说明生产的产品被消费者消费了,因此再进行一次生产
            num++;
            System.out.println(Thread.currentThread().getName()+"->生产了产品:num = "+num);
            //生产完毕后需要通知消费者进行消费
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();  //释放锁
        }
    }

    //消费
    public void decrement() throws InterruptedException {
        lock.lock();    //加锁
        try {
            //业务代码
            while (num == 0){
                //说明生产者未生产产品,因此等待生产者生产产品
                condition.await();
            }
            //说明生产已经生产了一个产品,因此再进行一次消费
            num--;
            System.out.println(Thread.currentThread().getName()+"->消费了产品:num = "+num);
            //消费完毕后需要通知生产者进行生产
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();     //释放锁
        }
    }
}

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

显然,我们同样也能使用JUC来实现生产者和消费者问题。但是这种新的方式只是和原来synchronized的方式功能一样,没有什么新的功能吗?要知道,任何一个新的技术,绝对不仅仅只是覆盖了原来的技术。

  • 我们来增加一个需求,现在我们要求:
    生产者1生产完毕后通知消费者1来消费,
    消费者1消费完毕后通知生产者2生产,
    生产者2生产完毕后通知消费者2来消费,
    消费者2消费完毕后通知生产者1来生产

从而达到一种线程间的精准通信的方式。(这就是JUC方式特有的新功能)

public class ProductorAndCustomer {
    public static void main(String[] args) {
        //资源
        Data data = new Data();
        new Thread(()->{
            try {
                for (int i = 0; i < 5; i++) {
                    data.printA();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try {
                for (int i = 0; i < 5; i++) {
                    data.printB();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try {
                for (int i = 0; i < 5; i++) {
                    data.printC();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}
//资源
class Data{
    private int num = 1;
    Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();
    //A执行完调用B,B执行完调用C,C执行完调用A
    public void printA() throws InterruptedException {
        lock.lock();
        try {
            while (num!=1){
                condition1.await();     //A等待
            }
            num=2;      //A执行
            System.out.println(Thread.currentThread().getName()+"->A执行,调用B");
            condition2.signal();    //唤醒B
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printB() throws InterruptedException {
        lock.lock();
        try {
            while (num!=2){
                condition2.await();
            }
            num=3;
            System.out.println(Thread.currentThread().getName()+"->B执行,调用C");
            condition3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printC() throws InterruptedException {
        lock.lock();
        try {
            while (num!=3){
                condition3.await();
            }
            num=1;
            System.out.println(Thread.currentThread().getName()+"->C执行,调用A");
            condition1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

运行结果
在这里插入图片描述
由此,我们使用同步监视器完成了线程之间的精准通信。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
1. 目的: 调试、修改、运行模拟程序,通过形象化的状态显示,使学生理解进程的概念,了解同步和通信的过程,掌握进程通信和同步的机制,特别是利用缓冲区进行同步和通信的过程。通过补充新功能,使学生能灵活运用相关知识,培养创新能力。 2. 内容及要求: 1) 调试、运行模拟程序。 2) 发现并修改程序不完善的地方。 3) 修改程序使用随机数控制创建生产者消费者的过程。 4) 在原来程序的基础上,加入缓冲区的写互斥控制功能,模拟多个进程存取一个公共缓冲区,当有进程正在写缓冲区时,其他要访问该缓冲区的进程必须等待,当有进程正在读取缓冲区时,其他要求读取的进程可以访问,而要求写的进程应该等待。 5) 完成1)、2)、3)功能的,得基本分,完成4)功能的加2分,有其它功能改进的再加2分 3. 程序说明:   本程序是模拟两个进程,生产者(producer)和消费者(Consumer)工作。生产者每次产生一个数据,送入缓冲区消费者每次从缓冲区取走一个数据。缓冲区可以容纳8个数据。因为缓冲区是有限的,因此当其满了时生产者进程应该等待,而空时,消费者进程应该等待;当生产者向缓冲区放入了一个数据,应唤醒正在等待的消费者进程,同样,当消费者取走一个数据后,应唤醒正在等待的生产者进程。就是生产者消费者之间的同步。   每次写入和读出数据时,都将读和写指针加一。当读写指针同样时,又一起退回起点。当写指针指向最后时,生产者就等待。当读指针为零时,再次要读取的消费者应该等待。 为简单起见,每次产生的数据为0-99的整数,从0开始,顺序递增。两个进程的调度是通过运行者使用键盘来实现的。 4. 程序使用的数据结构 进程控制块:包括进程名,进程状态和执行次数。 缓冲区:一个整数数组。 缓冲区说明块:包括类型,读指针,写指针,读等待指针和写等待指针。 5. 程序使用说明   启动程序后,如果使用'p'键则运行一次生产者进程,使用'c'键则运行一次消费者进程。通过屏幕可以观察到两个进程的状态和缓冲区变化的情况。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值