生产者-消费者

本文介绍了Java中实现生产者消费者模式的多种方式,包括wait/notifyAll、Condition、Semaphore及BlockingQueue等,并详细分析了每种方式的特点和应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程间通信是多线程十分重要的一个知识点,Java多线程是用基于wait/notify/notifyAll的等待/通知模式实现的。其一个经典的案例就是“生产者消费者模式”。其中,生产者负责生产商品,消费者负责消费商品。在没有商品时,消费者必须等待生产者生产;而在已经有商品时,生产者必须等待消费者消费完才能继续消费。当然,还有“变种”模式,就是生产者可以生产商品堆积,但是堆积的数目不能超过一定的数目。

wait/notifyAll

public class ProducerConsumerOfWaitNotifyAll {
    private static int LOOPS = 5;

    /**
     * 公共资源类
     */
    public static class PublicResource {
        private int number = 0;

        /**
         * 增加公共资源
         */
        public synchronized void increase() {
            /**
             * 如果用 if (number > 0) 出现的问题:
             * 始终只有一个线程会抢到main函数中的resource对象的this锁而得到执行权,
             * 例如生产者一抢到,而其他5个线程都在等待状态(另外2个生产者线程和3个消费者线程),
             * 当前生产者完成以后,就会唤醒等待的线程,而这个时候有5个线程在等待,
             * 不幸的是,执行权可能被另外一个生产者线程抢到,更不幸的是,执行权一直被这两个生产者轮流抢到,
             * 而消费者线程却一直“眼巴巴”的在旁边看着得不得执行的权力。
             * 当然,也可能出现一直是消费者线程在执行的情况。
             */
            while (number > 0) {//多线程并发,不能用if,必须用循环测试等待条件,避免虚假唤醒
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            number++;
            System.out.print("number ==> " + number + "\t");
            /**
             * 用 notifyAll() 唤醒多个线程同时竞争资源
             * 避免用 notify() 只唤醒一个线程,极端情况下生产者唤醒生产者,消费者唤醒消费者,造成假死
             */
            notifyAll();
        }

        /**
         * 减少公共资源
         */
        public synchronized void decrease() {
            while (number == 0) {//多线程并发,不能用if,必须用循环测试等待条件,避免虚假唤醒
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            number--;
            System.out.print("number ==> " + number + "\t");
            notifyAll();
        }
    }

    /**
     * 生产者线程,负责生产公共资源
     */
    public static class ProducerThread implements Runnable {
        private PublicResource resource;
        private String name;

        public ProducerThread(PublicResource resource, String name) {
            this.resource = resource;
            this.name = name;
        }

        @Override
        public void run() {
            for (int i = 0; i < LOOPS; i++) {
                resource.increase();
                System.out.println(name + "生产完成");
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 消费者线程,负责消费公共资源
     */
    public static class ConsumerThread implements Runnable {
        private PublicResource resource;
        private String name;

        public ConsumerThread(PublicResource resource, String name) {
            this.resource = resource;
            this.name = name;
        }

        @Override
        public void run() {
            for (int i = 0; i < LOOPS; i++) {
                resource.decrease();
                System.out.println(name + "消费完成");
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 模拟多个生产者和消费者操作公共资源的情形
     */
    public static void main(String[] args) {
        PublicResource resource = new PublicResource();

        new Thread(new ProducerThread(resource, "生产者一")).start();
        new Thread(new ConsumerThread(resource, "消费者一")).start();
        new Thread(new ProducerThread(resource, "生产者二")).start();
        new Thread(new ConsumerThread(resource, "消费者二")).start();
        new Thread(new ProducerThread(resource, "生产者三")).start();
        new Thread(new ConsumerThread(resource, "消费者三")).start();
    }
}
输出:
number ==> 1	生产者一生产完成
number ==> 0	消费者一消费完成
number ==> 1	生产者二生产完成
number ==> 0	消费者二消费完成
number ==> 1	生产者三生产完成
number ==> 0	消费者三消费完成
number ==> 1	生产者二生产完成
number ==> 0	消费者一消费完成
number ==> 1	生产者二生产完成
number ==> 0	消费者三消费完成
number ==> 1	生产者三生产完成
number ==> 0	消费者一消费完成
number ==> 1	生产者二生产完成
number ==> 0	消费者二消费完成
number ==> 1	生产者一生产完成
number ==> 0	消费者一消费完成
number ==> 1	生产者三生产完成
number ==> 0	消费者三消费完成
number ==> 1	生产者一生产完成
number ==> 0	消费者一消费完成
number ==> 1	生产者二生产完成
number ==> 0	消费者三消费完成
number ==> 1	生产者三生产完成
number ==> 0	消费者二消费完成
number ==> 1	生产者一生产完成
number ==> 0	消费者二消费完成
number ==> 1	生产者三生产完成
number ==> 0	消费者三消费完成
number ==> 1	生产者一生产完成
number ==> 0	消费者二消费完成

Condition

通过wait/notifyAll的方式实现多个生产者和消费者线程之间的通信的方式比较“浅层”,会造成资源上的浪费,每次都是唤醒全部线程,无法区分生产者和消费者。

通过Condtion对象,能更有针对性的唤醒线程,细化锁的粒度。具体的方式是,在同一把锁上绑定两个Condition对象,分别对应生产者和消费者,唤醒时通过Condition对象指定具体唤醒哪一类线程。这样做的针对性和效率更好。

public class ProducerConsumerOfCondition {
    private static int LOOPS = 5;

    /**
     * 公共资源类
     */
    private static class PublicResource {
        private int number = 0;

        private Lock lock = new ReentrantLock();

        private Condition proCondition = lock.newCondition();

        private Condition conCondition = lock.newCondition();

        /**
         * 增加公共资源,使用lock,而不是synchronized
         */
        public void increase() {
            lock.lock();// 通过lock方法获取锁对象,代替synchronized关键字
            try {
                while (number > 0) {
                    proCondition.await();
                }
                number++;
                System.out.print("number ==> " + number + "\t");

                conCondition.signalAll();// 只唤醒消费者线程,而不是唤醒所有线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }

        /**
         * 减少公共资源,使用lock,而不是synchronized
         */
        private void decrease() {
            lock.lock();// 通过lock方法获取锁对象,代替synchronized关键字
            try {
                while (number == 0) {
                    conCondition.await();
                }
                number--;
                System.out.print("number ==> " + number + "\t");

                proCondition.signalAll();// 只唤醒生产者线程,而不是唤醒所有线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    /**
     * 生产者线程,负责生产公共资源
     */
    private static class ProducerThread implements Runnable {
        private PublicResource resource;
        private String name;

        public ProducerThread(PublicResource resource, String name) {
            this.resource = resource;
            this.name = name;
        }

        @Override
        public void run() {
            for (int i = 0; i < LOOPS; i++) {
                resource.increase();
                System.out.println(name + "生产完成");
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 消费者线程,负责消费公共资源
     */
    private static class ConsumerThread implements Runnable {
        private PublicResource resource;
        private String name;

        public ConsumerThread(PublicResource resource, String name) {
            this.resource = resource;
            this.name = name;
        }

        @Override
        public void run() {
            for (int i = 0; i < LOOPS; i++) {
                resource.decrease();
                System.out.println(name + "消费完成");
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 模拟多个生产者和消费者操作公共资源的情形
     */
    public static void main(String[] args) {
        PublicResource resource = new PublicResource();

        new Thread(new ProducerThread(resource, "生产者一")).start();
        new Thread(new ConsumerThread(resource, "消费者一")).start();
        new Thread(new ProducerThread(resource, "生产者二")).start();
        new Thread(new ConsumerThread(resource, "消费者二")).start();
        new Thread(new ProducerThread(resource, "生产者三")).start();
        new Thread(new ConsumerThread(resource, "消费者三")).start();
    }
}
输出:
number ==> 1	生产者一生产完成
number ==> 0	消费者一消费完成
number ==> 1	生产者二生产完成
number ==> 0	消费者二消费完成
number ==> 1	生产者三生产完成
number ==> 0	消费者三消费完成
number ==> 1	生产者三生产完成
number ==> 0	消费者二消费完成
number ==> 1	生产者一生产完成
number ==> 0	消费者三消费完成
number ==> 1	生产者二生产完成
number ==> 0	消费者一消费完成
number ==> 1	生产者三生产完成
number ==> 0	消费者二消费完成
number ==> 1	生产者二生产完成
number ==> 0	消费者三消费完成
number ==> 1	生产者三生产完成
number ==> 0	消费者一消费完成
number ==> 1	生产者一生产完成
number ==> 0	消费者二消费完成
number ==> 1	生产者三生产完成
number ==> 0	消费者二消费完成
number ==> 1	生产者二生产完成
number ==> 0	消费者三消费完成
number ==> 1	生产者一生产完成
number ==> 0	消费者三消费完成
number ==> 1	生产者二生产完成
number ==> 0	消费者一消费完成
number ==> 1	生产者一生产完成
number ==> 0	消费者一消费完成

Semaphore

信号量是操作系统中很重要的一个概论,在Java中,可以通过J.U.C库中的Semaphore这个类很轻松完成对信号量的控制。通常,Semaphore类可以用来控制某个资源可被同时访问的个数,例如可以控制某个共享文件同时被多少个客户端访问。

这里用网上烂大街的例子来解释一个Semaphore类的用途。例如有个卫生间,有4个坑位,有8个人排队上卫生间。那么最多同时有4个人可以同时上卫生间,只有当其中的某个人完事了,在外面排队的人还可以进去。抽象成Semaphore类就是有4个信号量,进卫生间相当于获取一个信号量,而出卫生间相当于释放一个信号量。至于排队的人是按顺序进还是随机进,涉及到公平锁和非公平锁的概念。

Semaphore的本质是“共享锁”,单个信号量可以实现互斥锁的功能,与ReentrantLock不同的是,这个互斥锁可以被一个线程获得而被另一个线程释放。关于这一点,可以假设卫生间只有一个坑位,这时候永远只有一个人同时上卫生间,ReentrantLock的实现是,只要这个人不出来,外面的人要永远等待下去,哪怕这个人晕倒在卫生间。而单个信号量的实现是,当这个人晕倒了以后,管理员(相当于另外一个线程)可以拿着钥匙把卫生间门打开。

和J.U.C库中的某些类相似,Semaphore也是通过AQS框架实现的。和ReentrantLock不同的是,ReentrantLock是排他锁,而Semaphore是共享锁。

构造函数

构造函数主要是传入信号量数目的参数,具体实现上分公平锁和非公平锁两种,因此构造函数也有以下两个:

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

Semaphore之生产者消费者

public class ProducerConsumerOfSemaphore {
    private static int LOOPS = 5;

    /**
     * 公共资源类
     */
    private static class PublicResource {
        private int number = 0;

        // 以生产者开始的信号量,初始信号量数量为1
        static Semaphore semProd = new Semaphore(1);
        // 消费者信号量,初始信号数量为0
        static Semaphore semCon = new Semaphore(0);

        /**
         * 增加公共资源
         */
        public void increase() {
            try {
                semProd.acquire();// 获取生产者信号量

                number++;
                System.out.print("number ==> " + number + "\t");

                semCon.release();// 释放消费者信号量
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        /**
         * 减少公共资源
         */
        private void decrease() {
            try {
                semCon.acquire();// 获取消费者信号量

                number--;
                System.out.print("number ==> " + number + "\t");

                semProd.release();// 释放生产者信号量
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 生产者线程,负责生产公共资源
     */
    private static class ProducerThread implements Runnable {
        private PublicResource resource;
        private String name;

        public ProducerThread(PublicResource resource, String name) {
            this.resource = resource;
            this.name = name;
        }

        @Override
        public void run() {
            for (int i = 0; i < LOOPS; i++) {
                resource.increase();
                System.out.println(name + "生产完成");
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 消费者线程,负责消费公共资源
     */
    private static class ConsumerThread implements Runnable {
        private PublicResource resource;
        private String name;

        public ConsumerThread(PublicResource resource, String name) {
            this.resource = resource;
            this.name = name;
        }

        @Override
        public void run() {
            for (int i = 0; i < LOOPS; i++) {
                resource.decrease();
                System.out.println(name + "消费完成");
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 模拟多个生产者和消费者操作公共资源的情形
     */
    public static void main(String[] args) {
        PublicResource resource = new PublicResource();

        new Thread(new ProducerThread(resource, "生产者一")).start();
        new Thread(new ConsumerThread(resource, "消费者一")).start();
        new Thread(new ProducerThread(resource, "生产者二")).start();
        new Thread(new ConsumerThread(resource, "消费者二")).start();
        new Thread(new ProducerThread(resource, "生产者三")).start();
        new Thread(new ConsumerThread(resource, "消费者三")).start();
    }
}
输出:
number ==> 1	生产者一生产完成
number ==> 0	消费者一消费完成
number ==> 1	生产者二生产完成
number ==> 0	消费者二消费完成
number ==> 1	生产者三生产完成
number ==> 0	消费者三消费完成
number ==> 1	生产者二生产完成
number ==> 0	消费者二消费完成
number ==> 1	生产者一生产完成
number ==> 0	消费者三消费完成
number ==> 1	生产者三生产完成
number ==> 0	消费者一消费完成
number ==> 1	生产者一生产完成
number ==> 0	消费者一消费完成
number ==> 1	生产者二生产完成
number ==> 0	消费者二消费完成
number ==> 1	生产者二生产完成
number ==> 0	消费者三消费完成
number ==> 1	生产者二生产完成
number ==> 0	消费者二消费完成
number ==> 1	生产者一生产完成
number ==> 0	消费者三消费完成
number ==> 1	生产者三生产完成
number ==> 0	消费者一消费完成
number ==> 1	生产者一生产完成
number ==> 0	消费者二消费完成
number ==> 1	生产者三生产完成
number ==> 0	消费者三消费完成
number ==> 1	生产者三生产完成
number ==> 0	消费者一消费完成

BlockingQueue

在Java1.5中提供了很多并发类,其中ArrayBlockingQueue就是其中的一个。ArrayBlockingQueue为BlockingQueue的实现类,常用的阻塞队列。使用这个类能很方便的实现生产者和消费者的经典模式。其提供的阻塞式方法put与take能屏蔽同步的细节。

ArrayBlockingQueue提供了对队列进行操作的一些重要方法,这些方法主要分为两大类,即读操作和写操作,下面简单介绍一下这些方法:

读操作写操作
peek(): 返回队首的元素,如果为空返回null
poll(): 这个方法和上面方法类似,只不过同时将队首元素删除
poll(long timeout, TimeUnit unit): 这个方法和上面方法类似,只不过当队列为空时会等待一段时间
take(E e): 获取队首的元素,如果队列为空则一直等待(即阻塞式)
add(E e): 将元素e插入队列,如果当前队列未满则立即插入返回true,否则抛出IllegalStateException
offer(E e): 将元素e插入队列,如果当前队列未满则立即插入返回true,否则插入失败返回false
offer(E e, long timeout, TimeUnit unit): 这个方法和上面方法类似,只不过有个等待时间
put(E e): 将元素e插入队列,如果队列满了则一直等待(即阻塞式)
public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        enqueue(e);
    } finally {
        lock.unlock();
    }
}
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }
}
可以看出put()和take()这两个方法中关于锁的获取与释放以及线程的等待和唤醒与生产者消费者之Condition实现中类似。其中this.lock是ArrayBlockingQueue的一个ReentrantLock对象,而notFull和notEmpty是绑定在这个锁上的两个Condition对象。而put方法中的insert方法以及take方法中的extract方法是对队列的操作,具体可以参考源代码。
public class ProducerConsumerOfBlockingQueue {
    private static int LOOPS = 5;

    private static class ProducerThread implements Runnable {
        private ArrayBlockingQueue<Integer> queue;
        private String name;

        public ProducerThread(ArrayBlockingQueue<Integer> queue, String name) {
            this.queue = queue;
            this.name = name;
        }

        @Override
        public void run() {
            for (int i = 0; i < LOOPS; i++) {
                produce();
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        private void produce() {
            try {
                int x = new Random().nextInt(1000);
                queue.put(x);
                System.out.println(x + "\t" + name + "生产完成");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static class ConsumerThread implements Runnable {
        private ArrayBlockingQueue<Integer> queue;
        private String name;

        public ConsumerThread(ArrayBlockingQueue<Integer> queue, String name) {
            this.queue = queue;
            this.name = name;
        }

        @Override
        public void run() {
            for (int i = 0; i < LOOPS; i++) {
                consume();
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        private void consume() {
            try {
                int x = queue.take();
                System.out.println(x + "\t" + name + "消费完成");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(1);
        new Thread(new ProducerThread(queue, "生产者一")).start();
        new Thread(new ConsumerThread(queue, "消费者一")).start();
        new Thread(new ProducerThread(queue, "生产者二")).start();
        new Thread(new ConsumerThread(queue, "消费者二")).start();
        new Thread(new ProducerThread(queue, "生产者三")).start();
        new Thread(new ConsumerThread(queue, "消费者三")).start();
    }
}
输出:
181	生产者一生产完成
181	消费者一消费完成
398	生产者二生产完成
398	消费者二消费完成
801	生产者三生产完成
801	消费者三消费完成
726	生产者一生产完成
726	消费者一消费完成
128	生产者二生产完成
128	消费者三消费完成
157	生产者一生产完成
157	消费者一消费完成
640	生产者一生产完成
640	消费者二消费完成
248	生产者三生产完成
248	消费者三消费完成
171	生产者三生产完成
171	消费者二消费完成
907	生产者一生产完成
561	生产者三生产完成
907	消费者二消费完成
561	消费者二消费完成
253	生产者二生产完成
253	消费者一消费完成
288	生产者二生产完成
288	消费者三消费完成
292	生产者三生产完成
292	消费者三消费完成
219	生产者二生产完成
219	消费者一消费完成


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值