使用 notifyAll 和 wait 模拟生产者和消费者的问题

实现此问题之前,有必要先了解一下 Java 描述的"锁"的原理

等待对列和同步队列的简单概述

线程A 执行 synchronized(obj) 相当于 JVM会创建 一个 Monitor(监视器) 对象,JVM 将 obj 对象的对象头的 MarkWord 字段 指向 Monitor,同时Monitor 的 Owner指向 线程A,而且 Owner只可以指向一个线程,这就相当于给 obj 对象加好锁了。

当前对象 obj 被上了一把锁后,那么,其他线程在执行synchronized(obj)时,由于 obj 关联的 Monitor 的 Owner 已经 有指向了,其他线程就必须要阻塞等待了,阻塞等待就涉及到 Monitor 的等待对列和同步队列。

  • 等待队列:线程A调用了对象 obj#wait()方法,线程A就会释放obj的锁,并进入到了obj 的 Monitor 的同步队列。
  • 同步队列:只有获取了对象 obj 的锁,线程才能执行对象的 synchronized中的代码,obj的锁每次只有一个线程可以获得,其他线程只能在等待队列中等待。

注:等待队列中的线程不会被操作系统调度,处于 WAITING 状态;只有同步队列中的线程才会被操作系统根据自己的调度策略去调度,同步队列的线程处于 BLOCKED状态。

notify,notifyAll和wait的基础

Java的Object类提供了了三个final方法,用于实现多线程下资源的互斥访问,或者说是多线程间的通信。

当然,使用这些方法有一个前提,就是使用它们之前,必须先获取 “锁”,否则会抛出java.lang.IllegalMonitorStateException异常。

例如:wait/notify/notifyAll 可以在 synchronized 同步块中使用。

调用 Object#wait() 发生了啥?

  1. 从同步队列中移除当前线程,封装当前线程的指针,并加入到等待队列里;
  2. 释放锁,并唤醒同步队列里的其他线程(唤醒就是让操作系统根据具体的调度策略去调度线程,成功被调度的就会获取锁,然后running);
  3. 挂起自己(就是让线程处于 WAITING 状态)。

调用 Object#notifyAll() 发生了啥?

  1. 将等待队列中的所有线程 移动到 同步队列;
  2. 然后,就是等待操作系统根据具体的调度策略去调度它们了。

简单的生产者和消费者实现

步骤

生产者:

  1. 判断资源是否充裕;
    如果资源充裕,就没必要再生产了,等待消费者消费完资源为止。
  2. 如果资源不足,就必须立即生产资源;
    资源生产完之后,必须通知消费者。

消费者:

  1. 判断资源是否充裕;
    如果资源不足,就不能再消费了,等待生产者生产出资源为止
  2. 如果资源充足;
    直接消费,之后,再通知生产者

问题

唤醒线程的问题(有可能会出现极端情况):

  • 每次唤醒的都是生产者线程,消费者线程一直处于就绪状态,如果生产者不判断生产的必要性,那么,资源就会越积越多,超过仓库的容量。
  • 也可能,每次唤醒的都是消费者线程,生产者生产完第一个资源,就一直处于就绪状态,如果消费者不判断是否可以消费,那么,就会出现 资源负数

解决

每次被唤醒后,都判断是否应该被唤醒,否则,就再次进入阻塞状态

  1. 方案一:(情况发生后,补救)
  • 生产者:每次被唤醒后,都判断(生产的必要性) 再生产资源是否会超过仓库的容量
  • 消费者:每次被唤醒后,都判断(是否可以消费)消费之后是否会出现 资源负数,即,我要的资源,是否都充足
  1. 方案二:(既然是Object#notifyAll 引起的问题,就不让这种情况发生)
  • 设置两把锁,消费者锁和生产者锁(类似于 lock 和 condition)
  • 所有生产者共享生产者锁,所有消费者共享消费者锁

源码

public class NotifyAndWaitTest {
    public static void main(String[] args) throws Exception {

        Data data = new Data();

        // A 线程,生产资源 10 个
        new Thread(()->{
            for (int i = 0; i < 555; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        // B 线程,消费资源 10 个
        new Thread(()->{
            for (int i = 0; i < 555; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();


        // C 线程,生产资源 10 个
        new Thread(()->{
            for (int i = 0; i < 666; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();

        // D 线程,消费资源 10 个
        new Thread(()->{
            for (int i = 0; i < 666; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"D").start();

    }

    static class Data{
        // 当前资源个数
        private int data = 0;
        // 资源仓库的最大容量为 3
        private final int MAX_SIZE = 3;

        // +1
        public synchronized void increment() throws InterruptedException {
            // 资源充裕,等待消费者消费
            // if (data >= 1){
            //     // 无限阻塞,直到被唤醒
            //     this.wait();
            // }
            while (data + 1 > MAX_SIZE){
                // 没必要生产,无限阻塞,直到被唤醒
                this.wait();
            }

            // 生产
            data++;
            System.out.println("[" + Thread.currentThread().getName() + "], data=" + data);
            // 随机唤醒一个线程
            // 这里其实应该唤醒一个消费者
            // 但是,由于唤醒是随机的,所以,可能唤醒生产者
            // 所以,在唤醒之后,生产者要判断是否有必要生产
            // if 应该换成 while
            this.notifyAll();
        }

        // -1
        public synchronized void decrement() throws InterruptedException {
            // 资源不足,等待生产者生产
            // if (data <= 0){
            //     this.wait();
            // }
            while (data - 1 < 0){
                // 资源不足,无限等待,直到被唤醒
                this.wait();
            }

            // 消费
            data--;
            System.out.println("[" + Thread.currentThread().getName() + "], data=" + data);
            // 随机唤醒一个线程
            // 这里其实应该唤醒一个生产者
            // 但是,由于唤醒是随机的,所以,可能唤醒消费者
            // 所以,在唤醒之后,消费者要判断是否可以消费
            // if 应该换成 while
            this.notifyAll();
        }
    }
}

附录

Java Synchronized 重量级锁原理深入剖析下(同步篇)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
生产者消费者问题是一个经典的并发编程问题,可以使用Java语言来模拟实现。下面是一个使用Java多线程实现生产者消费者问题的示例代码: ```java import java.util.LinkedList; import java.util.Queue; public class ProducerConsumer { public static void main(String[] args) { Queue<Integer> queue = new LinkedList<>(); int maxSize = 10; Thread producerThread = new Thread(new Producer(queue, maxSize), "Producer"); Thread consumerThread = new Thread(new Consumer(queue), "Consumer"); producerThread.start(); consumerThread.start(); } static class Producer implements Runnable { private Queue<Integer> queue; private int maxSize; public Producer(Queue<Integer> queue, int maxSize) { this.queue = queue; this.maxSize = maxSize; } @Override public void run() { while (true) { synchronized (queue) { while (queue.size() == maxSize) { try { System.out.println("Queue is full, waiting for consumer to consume..."); queue.wait(); } catch (InterruptedException ex) { ex.printStackTrace(); } } int number = (int) (Math.random() * 100); queue.add(number); System.out.println("Produced " + number); queue.notifyAll(); } } } } static class Consumer implements Runnable { private Queue<Integer> queue; public Consumer(Queue<Integer> queue) { this.queue = queue; } @Override public void run() { while (true) { synchronized (queue) { while (queue.isEmpty()) { try { System.out.println("Queue is empty, waiting for producer to produce..."); queue.wait(); } catch (InterruptedException ex) { ex.printStackTrace(); } } int number = queue.remove(); System.out.println("Consumed " + number); queue.notifyAll(); } } } } } ``` 上面的代码中,生产者线程和消费者线程都是使用`Runnable`接口实现的,并且都使用`synchronized`关键字来保证线程安全。生产者线程在队列满时等待,消费者线程在队列空时等待,当有新的元素加入队列时,生产者线程会唤醒等待的消费者线程,当有元素被消费时,消费者线程会唤醒等待的生产者线程。这种实现方式可以有效避免死锁和饥饿等并发编程问题
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值