使用阻塞线程安全队列与生产者和消费者通信

Java 9并发编程指南 目录

使用阻塞线程安全队列与生产者和消费者通信

生产者/消费者问题是并发编程中的经典问题。有一个或多个数据生产者,它们将数据存储在缓冲区中。还有一个或多个从相同缓冲区获取数据的消费者。生产者和消费者共享相同的缓冲区,因此必须控制对缓冲区的访问以避免数据不一致的问题。当缓冲区为空时,消费者等待直到缓冲区里有元素,如果缓冲区已满,生产者等待直到缓冲区有空间。

这个问题已经通过Java和其它语言开发的几乎所有技术和同步机制实现过(请参阅本节“更多关注”获得更多信息)。这个问题的一个优点是其可以应用到真实场景中。

LinkedTransferQueue类是Java 7并发API引入的数据结构,目的是用来解决这类问题,其主要特性如下所示:

  • 阻塞数据结构。线程被阻塞,直到能够进行操作为止,前提是操作可以立即执行。
  • 长度无限制,可插入任意多元素。
  • 参数化类,需要指明待存入列表中的元素类。

本节将学习如何使用LinkedTransferQueue类运行许多共享字符串缓冲区的生产者和消费者任务。

准备工作

本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。

实现过程

通过如下步骤实现范例:

  1. 创建名为Producer的类,指定其实现Runnable接口:

    public class Producer implements Runnable {
    
  2. 声明两个私有String属性:initPath存储要检索的初始文件夹,end存储任务寻找文件的后缀名:

    	private LinkedTransferQueue<String> buffer;
    
  3. 声明名为results的私有List属性,存储任务已经找到的文件完整路径:

    	private String name;
    
  4. 实现类构造函数,初始化属性:

    	public Producer(String name, LinkedTransferQueue<String> buffer){
    		this.name=name;
    		this.buffer=buffer;
    	}
    
  5. 实现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);	
    	}
    }
    
  6. 实现名为Consumer的类,指定其实现Runnable接口:

    public class Consumer implements Runnable {
    
  7. 声明名为buffer的String类参数化的私有LinkedTransferQueue属性:

    	private LinkedTransferQueue<String> buffer;
    
  8. 声明名为name的私有String属性,存储消费者名称:

    	private String name;
    
  9. 实现类构造函数,初始化属性:

    	public Consumer(String name, LinkedTransferQueue<String> buffer){
    		this.name=name;
    		this.buffer=buffer;
    	}
    
  10. 实现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);	
    	}
    }
    
  11. 实现本范例主类,创建名为Main的类,包含main()方法:

    public class Main {
    	public static void main(String[] args) {
    
  12. 声明名为THREADS的常量,赋值100。创建具有String类对象的LinkedTransferQueue对象,名为buffer:

    		final int THREADS=100;
    		LinkedTransferQueue<String> buffer=new LinkedTransferQueue<>();
    
  13. 创建100个线程对象的数组,用来执行100个生产者任务:

    		Thread producerThreads[]=new Thread[THREADS];
    
  14. 创建100个线程对象的数组,用来执行100个消费者任务:

    		Thread consumerThreads[]=new Thread[THREADS];
    
  15. 创建、加载100个Consumer对象,将线程存储到前面创建的数组中:

    		for (int i=0; i<THREADS; i++){
    			Consumer consumer=new Consumer("Consumer "+i,buffer);
    			consumerThreads[i]=new Thread(consumer);
    			consumerThreads[i].start();
    		}
    
  16. 创建、加载100个Producer对象,将线程存储到前面创建的数组中:

    		for (int i=0; i<THREADS; i++) {
    			Producer producer=new Producer("Producer: "+ i , buffer);
    			producerThreads[i]=new Thread(producer);
    			producerThreads[i].start();
    		}	
    
  17. 使用join()方法等到线程执行结束:

    		for (int i=0; i<THREADS; i++){
    			try {
    				producerThreads[i].join();
    				consumerThreads[i].join();
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}		
    
  18. 输出缓冲区大小到控制台:

    		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条字符串。

本范例中,首先加载消费者然后是生产者,因此,如果缓冲区为空,所有消费者将被阻塞,直到生产者开始执行并将字符串存储在列表中。

下图显示本范例在控制台输出的部分执行信息:

pics/10_02.jpg

使用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。

更多关注

  • 第二章“基础线程同步”中的“同步程序中使用状态”小节
  • 第三章“线程同步功能”中的“并发任务间交换数据”小节
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值