小白的JUC学习03_精准通知下的生产消费者模式

生产者消费者模式


一、介绍

生产者消费者模式是一种设计模式,开发中我们需要一种这样的场景:

  • 一个模块用于产生数据,称为生产模块
  • 一个模块用于消费生产模块生成的数据,称为消费模块

处理这些模块,可以是函数,可以是线程或者进程,我们把处理生产模块的角色称为生产者,处理消费模块的角色称为消费者

但是在这之中,最重要的不是生产者也不是消费者,而是数据。这个数据到底如何定义:

  1. 必须是共享的数据
  2. 生产、消费模块对其进行处理时,应该要达到“相反”功能,也就是对数据处理时需要变化

数据是对:消费者、生产者 ,两者进行解耦。两方只关心数据,而不与对方产生直接依赖,降低耦合性

1.1、使用传统wait、notify实现生产者消费者模式


下面案例实现一个生产者消费者模式,通过操作指定对象的成员数据(对象内唯一 / 共享)
通过waitnotifyAll 进行通知生产消费,对其操作成员数据

新学到的玩意:

  • wait:使当前线程释放锁并进入等待状态,直到唤醒再继续执行
  • notifyAll唤醒所有等待的线程,然后当前线程继续执行不释放锁,除非遇到wait,或者代码块Over
    • notify:只唤醒一个等待的线程
package com.migu;

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

    public synchronized void increment () throws InterruptedException {
        // 资源“过多”就暂停生产数据, 通知 其他线程消费数据
        while (num != 0) {
            this.wait(); // 释放锁,进入等待队列
        }
        num++;
        System.out.println(Thread.currentThread().getName() + "生产之后: " + num);
        this.notifyAll();  // 通知/唤醒 除当前线程的其他所有线程
    }
    public synchronized void decrement () throws InterruptedException {
        // 没资源了就暂停消费数据 通知 其他线程去产生数据
        while (num == 0) {
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName() + "消费之后: " + num);
        this.notifyAll(); // 通知/唤醒 除当前线程的其他所有线程
    }
}

以上案例注意点:

  • 注意的是:锁的是什么
    • 使用waitnotify时,当前线程必须持有对应的对象锁,此时对象才可以调用这些方法
    • 否则会抛出java.lang.IllegalMonitorStateException异常:非法监视器状态异常
    • 以下案例中: 由于Synchronized关键字应用于成员方法,所以锁的是Data对象,那么在当前的同步代码中,只有Data对象才可以调用wait and notifyAll
  • 还有一个重要的问题:虚假唤醒
    • 虚假唤醒指的是:当存在多个线程时,生产者线程只产生了一份内容时,却导致了大量的消费者线程唤醒,并执行消费。
    • 所以在如上案例中,判断num的状态,我并没有使用if语句,而是采用了while。否则num状态就可能为负数
    • 原因是:
      • 因为if只会执行一次,执行完会接着向下执行if语句外边的代码
      • while不会,直到条件满足才会结束判断的
    • 所以:等待应该总是出现在循环中

但是以上案例有个缺点,就是在多个线程操作时的环境下,想要实现顺序执行时无法实现的

1.2、Lock版实现生产者消费者问题


一、Interface Condition介绍

  1. Condition接口也位于java.util.concurrent.locks包下
  2. 用于线程等待和唤醒做处理的接口,有主要两个方法
    • await取代wait
    • signal取代notify

二、代码实现

依照lock执行逻辑编写即可

package com.migu;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ProducerAndConsumer2 {
    public static void main(String[] args) {
        Data2 data = new Data2();
        // 生产者线程
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                data.increment();
            }
        }).start();
        // 消费者线程
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                data.decrement();
            }
        }).start();

    }
}

// 数据资源
class Data2 {
    private int num;
    Lock lock = new ReentrantLock();  // 加锁、解锁
    Condition condition = lock.newCondition(); // 等待、唤醒

    public void increment() {
	    lock.lock();
        try {
            // 业务逻辑--------------
            while (num != 0) {
                condition.await();
            }
            num++;
            System.out.println(Thread.currentThread().getName() + "生产之后: " + num);
            condition.signal();
            //-----------------------
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void decrement() {
	    lock.lock();
        try {
            while (num == 0) {
                condition.await();
            }
            num--;
            System.out.println(Thread.currentThread().getName() + "消费之后: " + num);
            condition.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

此时的效果的waitnotify是一样的,所以接下来要实现精准通知的实现

1.3、Condition的精准通知


重新理解Condition对象的API

  • Condition调用await方法时,是对当前持有lock锁的这个线程进入等待队列,并释放锁
  • 如果此时想要唤醒线程A,那么就需找到线程ALock锁,而这个Lock锁就在Condition
  • 那么也就是当ConditionA线程A执行等待时,那么就要用ConditionA去执行signal,才可以唤醒线程A
  • signal():唤醒在此Condition上的Lock对象上等待的单个线程。
  • signalAll():唤醒在此Condition上的Lock对象上等待的所有线程。
  • 以上就是所说的精准通知

实现线程轮流执行

场景:一共三个线程分别处理不同生产、消费模块,按顺序执行

  • 生产线程,每次产出数据
  • 其他两个线程依次对其消费
  • 注意要点:
    • 就是处理好while语句的条件,任何一种情况都要判断到
    • 对应好每个线程上condition持有的锁
package com.migu.procon;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ProducerAndConsumer2 {
    public static void main(String[] args) {
        Data2 data = new Data2();
        // 生产者线程
        new Thread(() -> {
            for (int i = 0; i < 2; i++) {
                data.increment();
            }
        }).start();
        // 消费者线程
        new Thread(() -> {
            while (true) {
                data.decrement();
            }
        }).start();
        // 消费者线程
        new Thread(() -> {
            while (true) {
                data.decrement2();
            }
        }).start();

    }
}

// 数据资源
class Data2 {
    private int num;
    Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();
    // 生产模块
    public void increment() {
        lock.lock();
        try {
            while (num != 0) {
                condition1.await();
            }
            num+=5;  // 每次产出五份数据
            System.out.println(Thread.currentThread().getName() + "生产之后: " + num);
            condition2.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    // 消费模块
    public void decrement() {
        lock.lock();
        try {
        	// 只处理第四份和第五份数据
            while (num == 0 || (num >=1 && num <=3)) {
                condition2.await();
            }
            num--;
            System.out.println(Thread.currentThread().getName() + "消费之后: " + num);
            condition3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }
    // 消费模块
    public void decrement2() {
        lock.lock();
        try {
            while(num == 0 || (num >= 4 && num <= 5)){
                condition3.await();
            }
            num--;
            System.out.println(Thread.currentThread().getName() + "消费之后: " + num);
            condition1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }
}

输出:
在这里插入图片描述
消费完毕,消费者线程进入等待,一直等待生产者线程产出数据

PS.这边例子只是为了演示顺序执行的流程,具体实际开发其实也并不清楚

还有一点:
在我们上面实现的生产、消费模式,一些列等待和通知都需要去手动实现
当数据量大的时候要去兼顾效率和安全问题,所以JUC也提供了一种方案叫做阻塞队列

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值