java并发编程day07

来源:互联网 发布:urllib.request json 编辑:程序博客网 时间:2024/06/07 22:13

5.3 阻塞队列和生产者-消费者模式

阻塞队列blockingqueue提供了可阻塞的put和take方法,他们与可定时的offer和pull是等价的。
如果Queue已经满了,put方法会被阻塞直到有空间可用;如果Queue是空的,那么take方法会被
阻塞,直到有元素可用。Queue的长度可以有限,也可以无限;无限的Queue永远不会满,所以它的
put方法永远不会阻塞。

阻塞队列支持生产者-消费者设计模式。一个生产者-消费者设计分离了“识别需要完成的工作”和“执行工作”。
该模式不会发现一个工作便立即处理,而是把工作置入一个任务清单中,以备后期处理。
生产者-消费者模式简化了开发,因为它解除了生产者类和消费者类之间相互依赖的代码。
生产者和消费者以不同的或者变化的速度生产和消费数据,生产者-消费者模式将这些活动解耦,因为
简化了工作负荷的管理。

生产者-消费者涉及是围绕阻塞队列展开的,生产者把数据放入队列,并使数据可用,当消费者为适当的行为
做好准备时会从队列中获取数据。生产者不需要知道消费者的身份或者数量,甚至根本没有消费者,它们只负责把
数据放入队列。类似的,消费者也不需要知道生产者是谁,以及是谁给它们安排的工作。
BlockingQueue 可以使用任何数量的生产者和消费者,从而简化了生产者-消费者设计的实现。
最常见的生产者-消费者设计是将线程池与工作队列相结合:讲述Executor任务执行框架时会具体
介绍这个设计模式,这是第6章和第8章的主题。

两个人洗盘子的劳动分工也是一个与生产者-消费者设计类似的例子:一个人洗盘子,并把洗好的盘子放在
盘子架上,另一个人从盘子架上得到盘子,并把它烘干。在这个场景中,盘子架充当了阻塞队列:
如果盘子架上没有盘子,消费者会一直等待,直到有盘子需要烘干,如果架子被放满了,生产者会停止
清洗直到架子上有新的空间。这样我们可以类推扩展到多个生产者和多个消费者:每一个工人只与
盘子架产生互动。他们不需要知道存在多少生产者和消费者,或者谁生产了某个给定工作条目。

“生产者”和“消费者”的标签是相对的:在某个上下文中存在一个作为消费者的活动,可能在里另一个上下文中成为
生产者。烘干盘子“消费”干净的湿盘子,产生干盘子。第三个人希望能帮助把干净的盘子收起来,
这样的话,现在就有两个共享的工作队列(每个都可以能阻塞烘干机的运行)

  • take会保持阻塞知道可用数据出现。如果生产者不能足够快的产生数据,让消费者忙碌起来,
    那么消费者只能一直等待,知道有工作可以做。但是如果生产者产生工作的速度总是比消费者处理的速度快,
    那么应用程序的工作条目会排在一个没有边界的队列中,最终耗尽内存。

  • put方法的阻塞特性 , 如果我们使用一个有界队列,那么当队列满的时候,生产者会阻塞,暂时不能生成
    更过的工作,从而个消费者时间来追赶进度。

  • offer 方法 如果条目不能被加入到队列中,它会返回一个失败状态。这使得我们能够创建更多灵活的策略
    来处理超负荷工作,比如减轻负载,序列化剩余工作条目并写入硬盘,减少生产者线程,
    或者用其他方法遏制生产者线程

  • 有界队列是强大的资源管理工具,用来建立可靠地应用程序:它们遏制那些可以产生过多
    工作量、具有威胁性的活动,从而让你的程序在面对超负荷工作时更加简装。

  • 生产者-消费者可以使代码解耦 但是还是间接的通过共享工作队列耦合在一起了。在我们的设计初期就是用阻塞队列建立对
    资源的管理–提早做这件事回避日后再修复容易得多。在某些情况下,阻塞队列使这更加简单,但是如果阻塞队列
    并不完全适合你的设计,你也可以用信号量创建其他的阻塞数据结构。

  • FIFO队列:LinkedBlockingQueue 和ArrayBlockingQueue,与LinkedList和ArrayList相似,但是却拥有比同步List更好的并发性能

  • 按优先级顺序排序的队列: PriorityBlockingQueue 可以比较元素自身的自然顺序(如果实现了Comparable),
    也可以试用一个Comparator进行排序

  • SynchronousQueue 它本质上不是一个真正的队列,因为他不会为队列元素维护任何存储空间。
    它维护一个排队的线程清单,这些线程等待把元素加入队列或者移出队列。它直接的移交工作,减少
    了在生产者-消费者之间移动数据的延迟时间。 SynchronousQueue没有存储的能力,所以除非另一个线程已经准备好
    参与移交工作,否则put和take会一直阻止。SynchronousQueue这类队列只有在消费者充足的时候比较适合,他们总能
    为下一个任务做好准备。

5.3.1 实例:桌面搜索

扫描本地驱动器并归档文件,为之后的搜索建立索引的代理,这类似于Google Desktop 或者Windows索引服务。
如下面代码DiskCrawler表现了一个生产者任务,这个任务是搜索一个文件结构,找到复合给定标准的文件并把他们的名称放入
工作队列;Indexer展现的是消费者从队列中取出文件名称并只做索引的任务

  • DiskCrawler 代码片段
public class FileCrawler implements Runnable {    private final BlockingQueue<File> fileQueue;    private final FileFilter fileFilter;    private final File root ;    @Override    public void run() {        crawl(root);    }    private void crawl(File root) throws InterruptedException{        File[] entries = root.listFiles(fileFilter);        if (entries!=null) {            for(File entry : entries){                if (entry.isDirectory()) {                    crawl(entry);                }else if (!alreadyIndex(entry)) {                    fileQueue.put(entry);                }            }        }    }}
  • Indexer 代码片段
public class Indexer implements Runnable{    private final BlockingQueue<File> queue;    public  Indexer(BlockingQueue<File> queue) {        this.queue = queue;    }    @Override    public void run() {        while(true){            try {                indexFile(queue.take());            } catch (InterruptedException e) {                Thread.currentThread().interrupt();            }        }    }}

生产者-消费者模式提供了线程友好的手段,从而能将桌面搜索问题细化成更简单的组件。语气让一个大活动完成各项工作,不如把文件查找和建立索引的事情划分给不同的活动,这会使代码更合理,更具有可读性;每一个活动只有一个单一的任务,并且阻塞队列掌控所有的控制流,所以每一个活动的代码都会更简单更清晰。

5.3.2 连续的线程限制

在java.util.concurrent中实现的阻塞队列,全部都包含充分的内部同步,从而能安全的将对象从生产者线程发布至消费者线程。

对于可变对象,生产者-消费者设计和阻塞队列一起,为生产者和消费者之间移交对象所有权提供了连续的线程限制 。一个线程约束的对象完全由单一线程所有,但是所有权可以通过安全的发布被转移,这样其他线程中只有唯一一个能够得到访问这个线程的权限,并且保证移交之后原线程不能再访问它,这样使得对象完全受限于新线程中。这个新的主人可以任意修改,因为它具有独占访问权。

对象池扩展了连续的线程限制,把对象借给一个请求线程。只要对象池中含有充分的内不同步,使对象池能够安全的发布,并且只要客户端本身不会发布对象池,或者在返回对象池后不再继续使用,所有权可以在线程间安全的传递。

我们也可以听过其他的发布机制来传递可变对象的所有权,但是必须确保只有一个线程接受到了对象的移交。阻塞队列简化了这项工作:只要多一点工作,它就可以通过ConcurrentMap的原子方法remove或AtomicReference的原子方法compareAndSet来完成。

5.3.3 双端队列和窃取工作

java6 新增了两个容器类型,Qeque 和BlockingDeque,它们分别扩展了Queue和BlockingQueue。Deque是一个双端队列,允许高效的在头和尾分别进行插入和移除。实现它们的是ArrayDeque和LinkedBlockingDeeque。

正如阻塞队列适用于生产者-消费者模式一样,双端队列使它们自身与一种叫做窃取工作的模式相关联。一个消费者生产设计中,所有的消费者只共享一个工作队列;在窃取工作的设计中,每一个消费者都有一个自己的双端队列。如果一个消费者完成了自己双端队列中的全部工作,它可以偷取其他消费者的双端队列咋红的末尾任务。因为工作者线程并不会竞争一个共享的任务队列,所以窃取工作模式比传统的生产者-消费者设计有更佳的可伸缩性;大多数时他们可以访问自己的双端队列,减少竞争。当一个工作必须要访问另一个队列时,它会从尾部截取,而不是从头部,从而进一步降低对双端队列的争夺。

窃取工作恰好适合用于解决消费者和生产者同体的问题—-当运行到一个任务的某单元时,可能会识别出更多的任务。比如:web crawler处理一个页面时,通常会发现有更多页面可以搜索。类似的还有许多图形扫描算法,比如在垃圾回收时对堆做了记号,可以并行使用窃取工作。当一个线程发现了一个新的任务单元时,它会把它放在自己队列的末尾(或者在另一种共享工作设计的情况下,可以放入其他工作者的队列中);当双端队列为空的时候,他回去其他队列的队尾寻找新的任务,这样能确保每一个线程都保持忙碌的状态。

原创粉丝点击