生产者/消费者问题是并发编程中的经典问题。有一个或多个数据生产者,它们将数据存储在缓冲区中。还有一个或多个从相同缓冲区获取数据的消费者。生产者和消费者共享相同的缓冲区,因此必须控制对缓冲区的访问以避免数据不一致的问题。当缓冲区为空时,消费者等待直到缓冲区里有元素,如果缓冲区已满,生产者等待直到缓冲区有空间。
这个问题已经通过Java和其它语言开发的几乎所有技术和同步机制实现过(请参阅本节“更多关注”获得更多信息)。这个问题的一个优点是其可以应用到真实场景中。
LinkedTransferQueue类是Java 7并发API引入的数据结构,目的是用来解决这类问题,其主要特性如下所示:
- 阻塞数据结构。线程被阻塞,直到能够进行操作为止,前提是操作可以立即执行。
- 长度无限制,可插入任意多元素。
- 参数化类,需要指明待存入列表中的元素类。
本节将学习如何使用LinkedTransferQueue类运行许多共享字符串缓冲区的生产者和消费者任务。
准备工作
本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。
实现过程
通过如下步骤实现范例:
-
创建名为Producer的类,指定其实现Runnable接口:
public class Producer implements Runnable {
-
声明两个私有String属性:initPath存储要检索的初始文件夹,end存储任务寻找文件的后缀名:
private LinkedTransferQueue<String> buffer;
-
声明名为results的私有List属性,存储任务已经找到的文件完整路径:
private String name;
-
实现类构造函数,初始化属性:
public Producer(String name, LinkedTransferQueue<String> buffer){ this.name=name; this.buffer=buffer; }
-
实现run()方法,使用buffer对象的put()方法在缓冲区中存储10000条字符串,输出指明方法执行结束的消息到控制台:
@Override public void run() { for (int i=0; i<10000; i++) { buffer.put(name+": Element "+i); } System.out.printf("Producer: %s: Producer done\n",name); } }
-
实现名为Consumer的类,指定其实现Runnable接口:
public class Consumer implements Runnable {
-
声明名为buffer的String类参数化的私有LinkedTransferQueue属性:
private LinkedTransferQueue<String> buffer;
-
声明名为name的私有String属性,存储消费者名称:
private String name;
-
实现类构造函数,初始化属性:
public Consumer(String name, LinkedTransferQueue<String> buffer){ this.name=name; this.buffer=buffer; }
-
实现run()方法,使用buffer对象的take()方法从缓冲区取出10000条字符串,输出指明方法执行结束的消息到控制台:
@Override public void run() { for (int i=0; i<10000; i++){ try { buffer.take(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.printf("Consumer: %s: Consumer done\n",name); } }
-
实现本范例主类,创建名为Main的类,包含main()方法:
public class Main { public static void main(String[] args) {
-
声明名为THREADS的常量,赋值100。创建具有String类对象的LinkedTransferQueue对象,名为buffer:
final int THREADS=100; LinkedTransferQueue<String> buffer=new LinkedTransferQueue<>();
-
创建100个线程对象的数组,用来执行100个生产者任务:
Thread producerThreads[]=new Thread[THREADS];
-
创建100个线程对象的数组,用来执行100个消费者任务:
Thread consumerThreads[]=new Thread[THREADS];
-
创建、加载100个Consumer对象,将线程存储到前面创建的数组中:
for (int i=0; i<THREADS; i++){ Consumer consumer=new Consumer("Consumer "+i,buffer); consumerThreads[i]=new Thread(consumer); consumerThreads[i].start(); }
-
创建、加载100个Producer对象,将线程存储到前面创建的数组中:
for (int i=0; i<THREADS; i++) { Producer producer=new Producer("Producer: "+ i , buffer); producerThreads[i]=new Thread(producer); producerThreads[i].start(); }
-
使用join()方法等到线程执行结束:
for (int i=0; i<THREADS; i++){ try { producerThreads[i].join(); consumerThreads[i].join(); } catch (InterruptedException e) { e.printStackTrace(); } }
-
输出缓冲区大小到控制台:
System.out.printf("Main: Size of the buffer: %d\n", buffer.size()); System.out.printf("Main: End of the example\n"); } }
工作原理
本节使用String类参数化的LinkedTransferQueue类实现生产者/消费者问题。LinkedTransferQueue类被用作缓冲区来共享生产者和消费者之间的数据。
我们实现了Producer类,使用put()方法将字符串添加到缓冲区。程序已经执行100个生产者,并且每个生产者向缓冲区插入10000条字符串,所以总共在缓冲区插入1000000条字符创。put()方法在缓冲区底部添加元素。
还实现了Consumer类,使用take()方法从缓冲区取出字符串,此方法返回并删除缓冲区的第一个元素。如果缓冲区为空,此方法阻塞调用的线程,直到缓冲区中有要使用的字符串为止。程序已经执行100个消费者,且每个消费者从缓冲区取出10000条字符串。
本范例中,首先加载消费者然后是生产者,因此,如果缓冲区为空,所有消费者将被阻塞,直到生产者开始执行并将字符串存储在列表中。
下图显示本范例在控制台输出的部分执行信息:
使用size()方法输出缓冲区的元素数量到控制台。需要注意的是,如果有线程正在列表中添加或删除数据的时候,此方法的返回值不是准确的。此方法必须遍历整个列表来计算元素数量,并且此操作可以更改列表内容。当且仅当没有任何线程修改列表时遍历,才能保证返回正确的结果。
扩展学习
LinkedTransferQueue还提供很多有用的方法, 如下所示:
- getWaitingConsumerCount():由于LinkedTransferQueue对象为空,返回阻塞在take()或者poll(long timeout, TimeUnit unit)方法中的消费者数量。
- hasWaitingConsumer():如果LinkedTransferQueue对象存在消费者等待,则返回true,否则返回false。
- offer(E e):将传参元素添加到LinkedTransferQueue对象的底部,且返回true值。E表示参数化LinkedTransferQueue类声明的类,或其子类。
- peek():返回LinkedTransferQueue对象的第一个元素,但不从列表中删除。如果队列为空,则返回null值。
- poll(long timeout, TimeUnit unit):如果LinkedTransferQueue缓冲区为空,则等待指定的时间周期,如果经过指定时间后,缓冲区依然为空,则返回null值。TimeUnit是一个枚举类型的类,包含如下常量:DAYS、HOURS、MICROSECONDS、MILLISECONDS、MINUTES、NANOSECONDS、和SECONDS。
更多关注
- 第二章“基础线程同步”中的“同步程序中使用状态”小节
- 第三章“线程同步功能”中的“并发任务间交换数据”小节