• Java 多线程学习笔记:生产者消费者问题


    前言:最近在学习Java多线程,看到ImportNew网上有网友翻译的一篇文章《阻塞队列实现生产者消费者模式》。在文中,使用的是Java的concurrent包中的阻塞队列来实现。在看完后,自行实现阻塞队列。

    (一)准备

      在本博文中,没有使用concurrent包中提供的阻塞队列,而是基于最近对多线程的学习,使用了ReentrantLock,开发实现了阻塞队列。如果想参考concurrent包中阻塞队列的使用方式,请点击上面的连接。

      在多线程中,生产者-消费者问题是一个经典的多线程同步问题。简单来说就是有两种线程(在这里也可以做进程理解)——生产者和消费者,他们共享一个固定大小的缓存区(如一个队列)。生产者负责产生放入新数据,消费者负责取出缓存区的数据。具体介绍请参考 Producer-consumer problem

    (二)设计概述

      详细的代码可以到我的 Github 仓库下载(master分支为详细的代码,输出所有的必要信息;simplification分支为简化的代码)。

      项目代码,分为四个类:Consumer类为消费者线程,Producer类为生产者线程,ProducerConsumer测试的启动类,ProducerConsumerQueue类为阻塞队列。

      在多线程中,生产者不断向队列放入新数据,当队列已满时,生产者将被阻塞,直到消费者从队列中取出部分数据并唤醒生产者;消费者不断从队列取出数据,当队列为空时,消费者将被阻塞,直到生产者再次放入新数据并唤醒消费者。在JAVA的concurrent包中,提供了一系列阻塞队列帮我们完成这一工作。这一工作也恰好是生产者消费者的问题的关键所在。在这里,我将自行实现阻塞队列:ProducerConsumerQueue;

    (三)详细实现

      (1)ProducerConsumerQueue类

     1 public final class ProducerConsumerQueue<E> {
     2     //声明一个重入锁
     3     private final Lock lock = new ReentrantLock();
     4     private final Condition notEmpty = lock.newCondition();
     5     private final Condition notFull = lock.newCondition();
     6     //缓存大小
     7     private final int CAPACITY;
     8     //存储数据的缓存队列
     9     private Queue<E> queue;
    10 
    11     public ProducerConsumerQueue(int CAPACITY) {
    12         this.CAPACITY = CAPACITY;
    13         this.queue = new LinkedList<E>();
    14     }
    15 
    16     //put the e into the queue
    17     public void put(E e) throws InterruptedException {
    18         lock.lock();
    19 
    20         try {
    21             while (queue.size() == this.CAPACITY)
    22                 this.notFull.await();
    23             queue.offer(e);
    24             this.notEmpty.signalAll();
    25         } finally {
    26             lock.unlock();
    27         }
    28     }
    29 
    30     //get e from queue.
    31     public E take() throws InterruptedException {
    32         lock.lock();
    33 
    34         try {
    35             while (queue.size() == 0)
    36                 this.notEmpty.await();
    37             this.notFull.signalAll();
    38             return queue.poll();
    39         } finally {
    40             lock.unlock();
    41         }
    42     }
    43 }

      该队列提供put()和take()方法,前者用于生产者放入数据,后者用于消费者取出数据。

      在put方法中,首先要获得锁。然后检测是否队列已满,如果是,阻塞生产者线程,并释放锁;当再次获得锁时,这里使用了while语句,使得线程再次检查队列是否已满,如果未满,放入数据到缓存,并唤醒可能阻塞的消费者线程。在这里使用while,目的是防止其他生产者线程已经唤醒,并且成功放入新数据到队列,然后阻塞,而该线程又再次放入,导致可能缓存溢出。

      在take方法中,同样要先获得锁。检测队列是否已空,如果是,阻塞消费者线程,释放锁;当被唤醒并再次获得锁时,使用while再次检查队列是否仍为空,如果非空,则取出缓存数据。while同样为了防止其他消费者对一个空队列取数据。

      (2)Consumer类

     1 public class Consumer implements Runnable {
     2     private final ProducerConsumerQueue sharedQueue;
     3 
     4     public Consumer(ProducerConsumerQueue sharedQueue) {
     5         this.sharedQueue = sharedQueue;
     6     }
     7 
     8     @Override
     9     public void run() {
    10         while (true) {
    11             try {
    12                 synchronized (this){
    13                     System.out.println("	Consumer: " + sharedQueue.take());
    14                 }
    15             } catch (InterruptedException ex) {
    16                 System.out.println(Thread.currentThread().getName() + " throw a interrupexception.");
    17                 //do something.
    18             }
    19         }
    20     }
    21 }

      消费者线程持续从队列中取出数据。

      (3)Producer类

     1 public class Producer implements Runnable {
     2     private final ProducerConsumerQueue sharedQueue;
     3 
     4     public Producer(ProducerConsumerQueue sharedQueue) {
     5         this.sharedQueue = sharedQueue;
     6     }
     7 
     8     @Override
     9     public void run() {
    10         for (int i = 0; i < 20; i++) {
    11             try {
    12                 synchronized (this){
    13                     System.out.println("Producer: "+i);
    14                     sharedQueue.put(i);
    15                 }
    16             } catch (InterruptedException ex) {
    17                 System.out.println(Thread.currentThread().getName() + " throw a interrupexception. ");
    18                 //do something.
    19             }
    20         }
    21     }
    22 }

      生产者线程产生数据并放入队列中。

      (4)ProducerConsumer测试类

    public class ProducerConsumer {
        public static void main(String[] args) {
            ProducerConsumerQueue sharedQueue = new ProducerConsumerQueue(4);
    
            Thread prodThread = new Thread(new Producer(sharedQueue));
            Thread consThread1 = new Thread(new Consumer(sharedQueue));
            Thread consThread2 = new Thread(new Consumer(sharedQueue));
    
            prodThread.start();
            consThread1.start();
            consThread2.start();
        }
    }

        创建一个生产者线程和两个消费者线程。

    扩展:

    1、Wikipedia:http://en.wikipedia.org/wiki/Producer%E2%80%93consumer_problem

    2、《聊聊并发——生产者和消费者模式》http://www.infoq.com/cn/articles/producers-and-consumers-mode

  • 相关阅读:
    关于leetcode中链表中两数据相加的程序说明
    数字波束合成的一些理解
    我们可以通过结构和指针构建强大的数据结构
    递归的理解
    dm9000网口收发控制以及mac地址过滤设置
    dsp28377控制DM9000收发数据——第三版程序,通过外部引脚触发来实现中断接受数据,优化掉帧现象
    dsp28377控制DM9000收发数据——第二版程序,能够实现手术功能,但是容易掉帧;使用读取中断寄存器的方式判断中断
    dsp28377控制DM9000收发数据
    树形导航栏(折叠)(jquery)
    折叠(树形导航栏)
  • 原文地址:https://www.cnblogs.com/scutwang/p/4109566.html
Copyright © 2020-2023  润新知