生产者消费者问题、Java实现

今天把生产者消费者问题搞定。还是一样的节奏,看一个问题要从历史看、全局看,这样才能掌握其全貌,最终各个击破,了然于胸。

先来回顾如下一些概念:


一、前置概念

1. 程序、进程和线程

  1. 程序 - Program

    程序是静态的源代码或目标程序,是一个“没有生命的实体”

  2. 进程 - Process

    当CPU赋予程序生命时也即操作系统执行它时,程序成为了一个“活动的实体”(但不是可执行的实体),称为进程 - 进行中的程序。

    • 进程是程序的一个实例。
    • 是计算机分配资源的基本单位。
    • 是线程的容器。
  3. 线程 - Thread

    线程是进程中的一个“可执行实体”,是系统独立调度和基本单位,线程自己不拥有系统资源。

    • 线程是现代操作系统的重要指标。
    • 一个进程中的所有线程共享进程所有资源。

2. 进程间通信

进程间通信(Inter-Process Communication, IPC)简称IPC。IPC是标准的UNIX通信机制,是一组编程接口,让程序员能够协调不同的进程,使之能在一个OS里同时运行,并相互传递、交换信息。

最初的IPC方法有两种:

  1. 信号 - Signals

    是UNIX系统中使用最早的一种IPC方法。

    OS中预先规定好了一系列事件(信号源),比如一个键盘中断或者一个错误都是,这些事件能产生一个信号。OS通过信号来通知当前进程系统中发生了某种预先规定好的事件。当进程识别出信号的到来,就采取适当的动作来传送或处理信号。

  2. 管道 - PIPE

    这里不表。

后来,在System V UNIX(1983,正统UNIX)中又首次引入了3种IPC机制:

  1. 消息队列 - message queues

    消息队列是消息的一个链表,它允许一个或多个进程向它写消息,一个或多个进程从中读取消息。

    Linux维护了一个MQ向量表:msgque表示消息队列。

  2. 信号量/旗语 - semaphore

    信号量本质上是一个“计数器”,用来记录某个资源的存取状况。有互斥信号量、条件信号量。

  3. 共享内存 - shared memory

    共享内存通常由一个进程创建,其余进程对这块内存进行读写。通常用信号量来管理共享内存。

另外,还有一种重要的方式:

  • Socket - 套接字

    TCP/IP是网络通讯协议,而Socket是这些协议的一种实现API,最早实现在BSD UNIX中。今天Socket是最通用的网络编程API,所有提供TCP/IP协议栈的OS中都提供了SocketAPI。

3. 线程间通信

进程可以通过以上这么多IPC手段来通讯,那么线程呢?

其实线程之间通讯很简单,因为同一个进程的线程们共享进程的资源,所以它们之间的通讯其实就是去“读写共享资源”。当然,也需要一些手段来保证数据一致性。

tips:JVM 中Heap 和Method Area 就是JVM进程(Process)的资源区域,也就是所有JVM线程(Thread)可以读写的共享资源。

“线程同步”是保证线程安全访问共享资源的一种手段,同步方法包括:

  • 事件 - Event
  • 互斥 - Mutex
  • 信号量 - Semaphore
  • 临界资源

二、生产者消费者问题

1. 抛出问题

生产者消费者问题(Producer-consumer problem)也可以叫有限缓冲问题(Bounded-buffer problem),是一个经典的进程/线程同步问题。

问题描述如下:

首先,工厂中有一个产品缓冲区生产者会不停地往缓冲区中添加新产品,而消费者则是不停的从缓冲区取走产品。

问:如何保证生产者不会在缓冲区满时加入数据?且消费者不会在缓冲区为空时消耗数据?

解答也很简单:

生产者必须在缓冲区满时休眠,直到消费者消耗了产品时才能被唤醒。
同时,也必须让消费者在缓冲区空时休眠,直到生产者往缓冲区中添加产品时才能唤醒消费者。

在程序设计层面来说,解决方案就是上面所说:“让生产者和消费者能够通信”,这其中最常用的是信号量方法,所以我们再来说说信号量。

2. 再说信号量 - semaphore

如上所述,信号量本质上是一个计数器,可以用来处理进程同步问题。

在OS中,给予每个进程一个信号量,代表每个进程目前的状态,未得到控制权的进程会在特定的地方被强迫停下来,等待可以继续进行的信号到来。

种类:

  1. 信号量可以是一个任意整数

    称为:计数信号量(Counting semaphore)或一般信号量(general semaphore)

  2. 信号量可以是二进制的0或1

    称为:二进制信号量(Binary semaphore)或互斥信号量 - Mutex。

PV原语:

1965年,荷兰计算机科学家艾兹格·迪杰斯特拉(Edsger W. Dijkstra)发明了Semaphore机制,现在广泛的应用在各种OS中。Dijkstra同时提出了两个原语(原子语句)来操作semaphore:

  1. P原语

    P是荷兰语Proeren(测试)的首字母。P是阻塞原语,负责把当前进程由运行状态转换为阻塞状态,直到另一个进程唤醒它。

    代表的操作为:申请一个空闲资源(信号量减1),若成功,则退出;若失败,则该进程被阻塞。

  2. V原语

    V是荷兰语Verhogen(增加)的首字母。V为唤醒原语,负责把一个被阻塞的进程唤醒,它有一个参数表,记录着等待被唤醒的进程。

    代表的操作为:释放一个被占用的资源(信号量加1),若发现有被阻塞的线程,则选择一个唤醒之。

三种使用:

将信号量的2种种类和PV原语结合,可以将对semaphore 的操作分为以下三种情况:

  1. 视semaphore为一个“加锁标志位”,实现对一个共享变量的互斥访问:

    // mutex的初始值为 1,访问该共享数据;
    int mutex = 1;
    
    P(mutex); // 尝试P 阻塞原语,mutex - 1
    V(mutex); // V 唤醒原语,mutex + 1
    //非临界区
  2. 视semaphore为“共享资源剩余个数”,实现对一个类共享资源的访问:
    过程:

    // mutex的初始值为资源的个数 N ,使用该资源;
    int mutex = N;
    
    P(mutex); 
    V(mutex);
    //非临界区
  3. 视semaphore为进程间的同步工具:

    临界区C1;
    P(S); 
    V(S);
    临界区C2;

三、Java中生产者消费者问题

生产者消费者问题有以下几种情况:

  • 1个生产者1个消费者。
  • 1个生产者N个消费者。
  • M个生产者N个消费者。

我们这里写几个第三种的例子:

  1. 不能使用java.util.concurrent包的情况下:

    import java.util.LinkedList;
    import java.util.Random;
    
    /**
     * 多生产者、多消费者
     * @author alanzhangyx
     */
    public class ProducerConsumer {
    
        //定义一个队列缓冲区,数据为Integer
        private Queue<Integer> queue = new LinkedList<>();
    
        //设置缓冲区最大容量
        private static final int MAX_SIZE = 100;
    
        /**
         * 生产者。
         *
         * <p>生产者进行V原语操作</p>
         * <ul>
         * <li>如果缓冲区没有达到MAX_SIZE,则生产一个产品(n个也行)放入缓冲区,并唤醒所有线程</li>
         * <li>否则使自己进入缓冲区的等待池</li>
         * </ul>
         *
         * @version  1.0.0
         * @author   alanzhangyx
         */
        class Producer implements Runnable{
            @Override
            public void run() {
                while (true) {
                    synchronized (queue){
                        if (queue.size() < MAX_SIZE) {
                            int num = new Random().nextInt(100);
                            queue.offer(num);
                            queue.notifyAll();
                            System.out.println("生产者" + Thread.currentThread().getName() + "生产了产品:" + num + ",此时缓冲区数据量为:" + queue.size());
                        } else {
                            try {
                                queue.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
    
        /**
         * 消费者。
         *
         * <p>消费者进行P原语操作</p>
         * <ul>
         * <li>如果缓冲区有数据,则从缓冲区取出一个产品(n个也行),并唤醒所有线程</li>
         * <li>否则使自己进入缓冲区的等待池</li>
         * </ul>
         *
         * @version  1.0.0
         * @author   alanzhangyx
         */
        class Consumer implements Runnable{
            @Override
            public void run() {
                while (true) {
                    synchronized (queue){
                        if (queue.size() > 0) {
                            int num = queue.poll();
                            System.out.println("消费者" + Thread.currentThread().getName() + "消费了产品:" + num + ",此时缓冲区数据量为:" + queue.size());
                            queue.notifyAll();
                        } else {
                            try {
                                queue.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
    
    
        public static void main(String[] args) {
            ProducerConsumer pc = new ProducerConsumer();
    
            //Thread构造函数需要一个Runnable对象即可构造一个新的线程,Runnable对象可以重复利用,不必new多个
            //一个消费者,一个生产者
            Consumer c = pc.new Consumer();
            Producer p = pc.new Producer();
    
            //生产者和消费者谁先start都一样
            new Thread(c).start();
            new Thread(c).start();
            new Thread(c).start();
            new Thread(c).start();
            new Thread(c).start();
    
            new Thread(p).start();
            new Thread(p).start();
            new Thread(p).start();
            new Thread(p).start();
            new Thread(p).start();
    
        }
    
    }
    
  2. 利用java.util.concurrent.BlockingQueue

    public class RelyBlockingQueue {
    
        public static void main(String[] args) {
            // 容量为100
            final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(100);
    
            final Producer producer1 = new Producer(queue);
            final Producer producer2 = new Producer(queue);
    
            final Consumer consumer1 = new Consumer(queue);
            final Consumer consumer2 = new Consumer(queue);
    
            producer1.start();
            producer2.start();
    
            consumer1.start();
            consumer2.start();
    
        }
    
    
        static class Producer extends Thread {
    
            private BlockingQueue<Integer> blockingQueue;
    
            public Producer(BlockingQueue<Integer> blockingQueue) {
                this.blockingQueue = blockingQueue;
            }
    
            @Override
            public void run() {
                while (true) {
                    try {
                        int num = new Random().nextInt(100);
                        blockingQueue.put(num);
                        System.out.println("生产者" + Thread.currentThread().getName() + "生产了产品:" + num + ",此时缓冲区数据量为:" + blockingQueue.size());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        static class Consumer extends Thread {
    
            private BlockingQueue<Integer> blockingQueue;
    
            public Consumer(BlockingQueue<Integer> blockingQueue) {
                this.blockingQueue = blockingQueue;
            }
    
            @Override
            public void run() {
                while (true) {
                    try {
                        final Integer num = blockingQueue.take();
                        System.out.println("消费者" + Thread.currentThread().getName() + "消费了产品:" + num + ",此时缓冲区数据量为:" + blockingQueue.size());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
    }

以上。

  • 10
    点赞
  • 57
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
以下是Java实现生产者消费者问题的示例代码: ```java import java.util.LinkedList; import java.util.Queue; import java.util.Random; import java.util.concurrent.Semaphore; public class ProducerConsumerExample { private static final int BUFFER_SIZE = 10; private static final Semaphore mutex = new Semaphore(1); private static final Semaphore empty = new Semaphore(BUFFER_SIZE); private static final Semaphore full = new Semaphore(0); private static final Queue<Integer> buffer = new LinkedList<>(); public static void main(String[] args) { Thread producer = new Thread(new Producer()); Thread consumer = new Thread(new Consumer()); producer.start(); consumer.start(); } static class Producer implements Runnable { private final Random random = new Random(); @Override public void run() { while (true) { try { int item = random.nextInt(); empty.acquire(); mutex.acquire(); buffer.add(item); System.out.println("Produced item: " + item); mutex.release(); full.release(); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } static class Consumer implements Runnable { @Override public void run() { while (true) { try { full.acquire(); mutex.acquire(); int item = buffer.remove(); System.out.println("Consumed item: " + item); mutex.release(); empty.release(); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } } ``` 上述代码中,我们使用了信号量实现线程之间的同步和互斥。其中,`mutex`是一个二元信号量,用于实现互斥访问共享缓冲区;`empty`是一个计数信号量,表示缓冲区中空闲的位置数;`full`也是一个计数信号量,表示缓冲区中已经存储的数据项数。 在生产者线程中,我们首先使用`empty.acquire()`获取一个空闲位置,然后使用`mutex.acquire()`获取互斥锁,向缓冲区中添加一个数据项,最后使用`mutex.release()`释放互斥锁,使用`full.release()`增加已存储的数据项数。 在消费者线程中,我们首先使用`full.acquire()`获取一个已存储的数据项,然后使用`mutex.acquire()`获取互斥锁,从缓冲区中移除一个数据项,最后使用`mutex.release()`释放互斥锁,使用`empty.release()`增加空闲位置数。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值