Java多线程——等待/通知机制(生产者消费者模型)

1. 等待/通知机制

  等待/通知机制在我们生活中很常见,例如,餐厅服务员和厨师之间,只有厨师做好菜之后,通知服务员,服务员才能上菜;而在未做好菜之前,服务员只能等待厨师做菜。除了这个例子外,等待/通知机制中,最典型的就是生产者和消费者模型,下边我们用代码实现该模型。

2. 单一生产者和消费者

  Java中等待/通知,通常使用Object类中的wait()方法阻塞线程,线程进入等待吃,notify()/notifyAll()方法唤醒线程。其中notify()notifyAll()方法的区别在于,notify()方法,值唤醒等待池线程中的一个线程,而notifyAll()唤醒所有等待池中的线程。下边我们使用代码,实现等待/通知中的典型例子,生产者和消费者模型。
  以生产汽车为例,创建一个汽车工厂类,其中有两个一个生产汽车和一个销售汽车的方法:

@Slf4j
public class CarFactory {

    private int num;

    private Object obj;

    public CarFactory(Object obj) {
        this.obj = obj;
    }

    public void createCar() {
        synchronized (obj) {
            try {
                while (num == 10) {
                    log.info("当前数量={},暂停生产", num);
                    obj.wait();
                }
                num++;
                log.info("生产者:{}, 生产了一辆汽车,当前总量:{}", Thread.currentThread().getName(), num);
                obj.notify();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void consumerCar() {
        synchronized (obj) {
            try {
                while (num == 1) {
                    log.info("当前数量={},暂停销售", num);
                    obj.wait();
                }
                num--;
                log.info("消费者:{}, 购买了了一辆汽车,当前总量:{}", Thread.currentThread().getName(), num);
                obj.notify();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

创建两个线程,分别指代生产者和消费者:

@AllArgsConstructor
public class Producer implements Runnable {

    private CarFactory carFactory;

    @Override
    public void run() {
        while (true) {
            carFactory.createCar();
        }
    }
}

@AllArgsConstructor
public class Consumer implements Runnable {

    private CarFactory carFactory;

    @Override
    public void run() {
        while (true) {
            carFactory.consumerCar();
        }
    }
}

测试方法:

public static void main(String[] args) throws Exception {
    CarFactory carFactory = new CarFactory(new Object());
    Producer producer = new Producer(carFactory);
    Consumer consumer = new Consumer(carFactory);
    new Thread(producer, "生产者A").start();
    new Thread(consumer, "消费者A").start();
}

  上边创建了一个生产者和一个消费者,程序运行的过程大致如下:

  1. 生产者获得锁之后,开始生产产品;
  2. 生产者生产的数量达到10之后,调用wait()方法,阻塞生产者线程,生产者释放锁;
  3. 生产者释放锁之后,消费者获得锁,消费一个后,调用notify()方法,唤醒一个等待池中的线程,而此时等待池中只有一个生产者,因此唤醒生产者,等待消费者释放锁;
  4. 消费者消费的只剩一个之后,调用wait()方法,阻塞消费者,释放锁;
  5. 生产者再次得到锁,然后再次从第1步开始执行。

  需要注意的地方:判断线程是否该阻塞时,需要使用while而不是if;因为,线程唤醒之后,会继续从上一次停止的地方开始执行;如果使用if,下一次唤醒之后,不会再次判断,会继续执行if后边的代码,这里就是会继续生产产品,造成误差,单个生产者消费者时,可能不明显,多个生产者消费者时会更加明显的体现。

以上测试代码,执行结果:

19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生产者:生产者A, 生产了一辆汽车,当前总量:2
19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生产者:生产者A, 生产了一辆汽车,当前总量:3
19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生产者:生产者A, 生产了一辆汽车,当前总量:4
19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生产者:生产者A, 生产了一辆汽车,当前总量:5
19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生产者:生产者A, 生产了一辆汽车,当前总量:6
19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生产者:生产者A, 生产了一辆汽车,当前总量:7
19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生产者:生产者A, 生产了一辆汽车,当前总量:8
19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生产者:生产者A, 生产了一辆汽车,当前总量:9
19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生产者:生产者A, 生产了一辆汽车,当前总量:10
19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 当前数量=10,暂停生产
19:24:33.673 [消费者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消费者:消费者A, 购买了了一辆汽车,当前总量:9
19:24:33.673 [消费者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消费者:消费者A, 购买了了一辆汽车,当前总量:8
19:24:33.673 [消费者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消费者:消费者A, 购买了了一辆汽车,当前总量:7
19:24:33.673 [消费者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消费者:消费者A, 购买了了一辆汽车,当前总量:6
19:24:33.673 [消费者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消费者:消费者A, 购买了了一辆汽车,当前总量:5
19:24:33.673 [消费者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消费者:消费者A, 购买了了一辆汽车,当前总量:4
19:24:33.673 [消费者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消费者:消费者A, 购买了了一辆汽车,当前总量:3
19:24:33.673 [消费者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消费者:消费者A, 购买了了一辆汽车,当前总量:2
19:24:33.673 [消费者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消费者:消费者A, 购买了了一辆汽车,当前总量:1
.........

3. 多个生产者和消费者

  上边展示了单个生产者消费者的情况,下边看一下多个生产者消费者的情况;之前我们说过notify()方法,只会唤醒等待池中的一个线程,在多个生产者消费者的情况下,如果继续使用notify()方法,每次就只能唤醒一个线程;
  更加严重的情况可能会出现“假死”,即,消费者唤醒了一个生产者,然后释放锁,生产者A开始生产,同时,调用notify()方法唤醒线程,如果唤醒了另一个生产者B;那么,当达到阻塞条件,生产者A进入WAITING状态,生产者B得到锁,开始执行同步块,但是刚执行就发现达到了阻塞条件也进入WAITING状态;此时,还没来得及唤醒线程;所有的线程都处于WAITING状态,就是线程“假死”。
  因此,多个生产者消费者我们需要使用notifyAll()方法,唤醒所有的线程,其他地方可以不做修改;测试方法,多创建几个生产者和消费者就可以了,这里不再重复代码。
  但是,使用notifyAll()方法,唤醒了所有的等待池中的线程,但是,生产者生产过程中,如果即将达到阻塞状态时,唤醒所有线程,唤醒的生产者还是会再次阻塞,浪费时间;如果我们只唤醒消费者就了,使用LockCondition可以实现我们的需求:

@Slf4j
public class CarFactory {

    private int num;

    private Lock lock = new ReentrantLock();

    private Condition producerCondition = lock.newCondition();

    private Condition consumerCondition = lock.newCondition();

    public void producerCar() {
        try {
            lock.lock();
            while (num == 10) {
                log.info("数量已达上限,停止生产");
                producerCondition.await();
            }
            num++;
            log.info("生产者:{}, 生产了一个汽车,总量为:{}", Thread.currentThread().getName(), num);
            consumerCondition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void consumerCar() {
        try {
            lock.lock();
            while (num == 1) {
                log.info("没车了,停止销售");
                consumerCondition.await();
            }
            num--;
            log.info("消费者:{}, 购买了一个汽车,总量为:{}", Thread.currentThread().getName(), num);
            producerCondition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

  上边代码,创建了两个Condition对象,分别用来阻塞和唤醒生产者消费者;生产方法中,使用producerCondition对象,阻塞生产者,但是使用consumerCondition对象的singAll()方法,来唤醒线程,因为消费者使用了也是该对象进行阻塞,因此,此时唤醒的都是消费者;对于消费者也是一样。
测试方法:

public static void main(String[] args) {
    CarFactory carFactory = new CarFactory();
    ExecutorService service = Executors.newCachedThreadPool();
    Producer producer = new Producer(carFactory);
    Consumer consumer = new Consumer(carFactory);
    for (int i = 0; i < 3; i++) {
        service.execute(producer);
        service.execute(consumer);
    }
    service.shutdown();
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
最近正在学习Java,也买了很多的有关Java方面的书籍,其中现《跟我学Java》这本书,都的很不错啊,所以顺便拿电脑把这本书的目录敲了下来,与大家分享。尤其是那些和我一样初学Java的朋友们,看看哪一节对你有用,不妨过来讨论一下哦! 第1章 Java概述 1.1 Java展史 1.1.1 Java起源 1.1.2 Java展 1.2 Java是什么 1.2.1 Java语言 1.2.2 Java平台 1.2.3 Java网络操作系统 1.3 Java的特点 1.4 Java程序类型 1.5 JDK与JRE 1.5.1 Java工具包 1.5.2 Java运行环境 1.6 Java技术体系 1.7 Java虚拟机 1.7.1 虚拟机数据类型 1.7.2 Java虚拟机的生命周期 1.7.3 Java虚拟机的体系结构 1.8 垃圾收集器 1.9 本章习题 第2章 Java环境 2.1 J2SE的下载和安装 2.1.1 J2SE的下载 2.1.2 J2SE的安装 2.2 环境变量的配置与测试 2.2.1 设置环境变量path 2.2.2 设置环境变量classpath 2.2.3 环境变量的测试 2.3 API文档的下载与使用 2.4 第一个应用程序 2.4.1 HelloWorld程序 2.4.2 程序运行编译命令 2.4.3 HelloWorld编译与运行 2.5 简单开工具介绍 2.5.1 EditPlus的编译与运行 2.5.2 在UltraEdit上开Java 2.6 集成开环境Eclipse介绍 2.6.1 Eclipse下载与安装 2.6.2 Eclipse的透视图 2.6.3 Eclipse的视图(View) 2.6.4 Eclipse的编辑器 2.6.5 Eclipse的工具栏和菜单栏 2.6.6 使用Eclipse编写HelloWorld程序 2.7 本章习题 第3章 Java语言基础 3.1 标识符 3.1.1 标识符的概念 3.1.2 变量命名规则 3.1.3 变量命名技巧 3.2 关键字 3.3 注释 3.4 数据类型 3.4.1 整型 3.4.2 浮点型 3.4.3 字符型 3.4.4 布尔型 3.5 变量与常量 3.5.1 变量声明 3.5.2 变量赋值和初始化 3.5.3 常量 3.6 类型转化 3.6.1 数值类型之间的转换 3.6.2 强制类型转换 3.7 运算符 3.7.1 算术运算符 3.7.2 关系运算符 3.7.3 逻辑运算符 3.7.4 位运算符 3.7.5 自动递增和递减 3.7.6 三元运算符 3.7.7 运算符的优先级 3.8 本章习题 第4章 程序流程控制 4.1 顺序结构 4.2 条件语句 4.2.1 if语句 4.2.2 if-else语句 4.2.3 if-else-if语句 4.2.4 if语句的嵌套 4.2.5 布尔表达式 4.2.6 开关语句 4.3 循环语句 4.3.1 while循环结构 4.3.2 do-while 循环结构 4.3.3 for循环结构 4.3.4 循环的嵌套 4.4 转向语句 4.4.1 break中断语句 4.4.2 continue条件继续语句 4.4.3 标签语句 4.5 返回语句 4.6 综合实例:水仙花数 4.7 本章习题 第5章 字符串 5.1 字符串简介 5.1.1 字符串常量 5.1.2 String创建字符串常量 5.1.3 StringBuffer创建字符串 5.2 连接字符串 5.2.1 与字符串的连接 5.2.2 与其他数据类型的连接 5.3 String字符串操作 5.3.1 基本操作 5.3.2 比较 5.3.3 转化 5.3.4 查找 5.3.5 截取拆分 5.3.6 替换或修改 5.4 StringBuffer类操作 5.4.1 基本操作 5.4.2 字符串操作方法 5.4.3 添加append() 5.4.4 插入insert() 5.5 实例:字符串应用 5.6 本章习题 第6章 数组 6.1 一

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值