uitl.concurrent研究(八)——超越 Map、Collection、List 和 Set的Queue

本文介绍了Java中Queue接口,它为集合提供新基本接口,有更多操作方法。Queue成员排序多样,实现通常不允许插入null。介绍了非阻塞队列如PriorityQueue、ConcurrentLinkedQueue,以及阻塞队列如ArrayBlockingQueue、LinkedBlockingQueue等,还给出了使用示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

超越 MapCollectionList SetQueue

一、版权说明:

本文部分文字摘自John Zukowski的《驯服 Tiger: 并发集合》(http://www-900.ibm.com/developerWorks/cn/java/j-tiger06164/index.shtml?ca=dwcn-newsletter-java

二、内容简介:
Doug Lea 最初编写的 util.concurrent 包变成了 JSR-166 ,然后又变成了 J2SE 平台的 Tiger 版本。这个新库提供的是并发程序中通常需要的一组实用程序。如果对于优化对集合的多线程访问有兴趣,那么您就找对地方了。

Java 编程的早期阶段,位于 Oswego 市的纽约州立大学(SUNY) 的一位教授决定创建一个简单的库,以帮助开发人员构建可以更好地处理多线程情况的应用程序。这并不是说用现有的库就不能实现,但是就像有了标准网络库一样,用经过调试的、可信任的库更容易自己处理多线程。在 Addision-Wesley 的一本相关书籍的帮助下,这个库变得越来越流行了。最终,作者 Doug Lea 决定设法让它成为 Java 平台的标准部分 —— JSR-166

三、介绍 Queue 接口:
java.util
包为集合提供了一个新的基本接口:java.util.Queue。虽然肯定可以在相对应的两端进行添加和删除而将 java.util.List 作为队列对待,但是这个新的 Queue 接口提供了支持添加、删除和检查集合的更多方法,如下所示:

 

public boolean offer(Object element)

public Object remove()

public Object poll()

public Object element()

public Object peek()

Queue接口被设计为一个存放对象成员的集合。作为基本的Collection操作,Queue提供了一些新的添加、删除、检查操作。以下为它们的方法名和规则列表:

操作

方法名

规则

添加

offer(x)

尝试添加,成功返回true,不成功返回false

add(x)

不成功抛出异常

删除

poll()

尝试删除,成功返回true,不成功返回false

remove()

不成功抛出异常

检查

peek()

尝试返回Queue头的成员,如果Queuenull,则返回null

element()

返回Queue头的成员,如果Queuenull,则抛出异常

四、Queue工作原理:

Queue的成员排序:Queue通常按照FIFO的方式排列成员,但不是必需的。比如优先级队列(priority queues)就是例外的队列,它按照比较器(comparator)来排列队列成员;还有按照队列成员本来的顺序排列;LIFO队列(后进先出队列)按照后进先出的顺序排列队列成员。无论使用哪种排列顺序,队列头的成员可以使用remove()poll()方法删除。在FIFO队列中,新成员被添加到队列的尾部。其它种类的队列不能使用不同的存放方式。每种Queue实现必须提供它自己的排列属性。

offer方法添加一个成员,如果成功返回true;如果失败返回false。这与Collection.add方法不同之处在于:Collection.add遇到失败的情况将抛出unchecked异常。所以offer方法被设计用在视失败为正常的情况下,而不是发生异常,例如一个固定长度的Queue

remove()poll()方法删除对列头部的成员。至于哪个成员被删除则完全按照队列的排序规则,这依照实现各有不同。remove()poll()方法的行为只有在队列为空情况下会有所不同:poll()方法返回null,而remove()方法将抛出异常。

element()peek()方法只返回队列头的成员,而不删除任何成员。

Queue接口没有定义阻塞队列(blocking queue)的方法,而这个方法是并发编程时通常要用到的。阻塞队列这个方法将使成员等待知道队列中有空间,此方法被定义在edu.emory.mathcs.backport.java.util.concurrent.BlockingQueue接口中(一个扩展了Queue接口的接口)J

Queue接口的实现通常不允许插入null成员到队列中,尽管一些例如LinkedList的实现不限制插入null。甚至在一些允许插入nullQueue接口的实现中,null不允许插入到Quqeue中,而且当队列不包含任何成员时poll()方法会返回null

Queue接口的实现通常不定义基于成员的equalshashCode方法,但从Object类继承了标识类的一些方法。因为在不同排序的队列中基于成员的equals不能很好的被定义。

使用基本队列
Tiger 中有两组 Queue 实现:实现了新 BlockingQueue 接口的和没有实现这个接口的。我将首先分析那些没有实现的。

在最简单的情况下,原来有的 java.util.LinkedList 实现已经改造成不仅实现 java.util.List 接口,而且还实现 java.util.Queue 接口。可以将集合看成这两者中的任何一种。清单 1 显示将 LinkedList 作为 Queue 使用的一种方法:

清单 1. 使用 Queue 实现
 

 

  Queue queue = new LinkedList();

  queue.offer("One");

  queue.offer("Two");

  queue.offer("Three");

  queue.offer("Four");

  // Head of queue should be One

  System.out.println("Head of queue is: " + queue.poll());

再复杂一点的是新的 java.util.AbstractQueue 类。这个类的工作方式类似于 java.util.AbstractList java.util.AbstractSet 类。在创建自定义集合时,不用自己实现整个接口,只是继承抽象实现并填入细节。使用 AbstractQueue 时,必须为方法 offer() poll() peek() 提供实现。像 add() addAll() 这样的方法修改为使用 offer(),而 clear() remove() 使用 poll()。最后,element() 使用 peek()。当然可以在子类中提供这些方法的优化实现,但是不是必须这么做。而且,不必创建自己的子类,可以使用几个内置的实现,其中两个是不阻塞队列: PriorityQueue ConcurrentLinkedQueue

PriorityQueue ConcurrentLinkedQueue 类在 Collection Framework 中加入两个具体集合实现。PriorityQueue 类实质上维护了一个有序列表。加入到 Queue 中的元素根据它们的天然排序(通过其java.util.Comparable 实现)或者根据传递给构造函数的 java.util.Comparator 实现来定位。将清单 2 中的 LinkedList 改变为 PriorityQueue 将会打印出 Four 而不是 One,因为按字母排列 —— 字符串的天然顺序 —— Four 是第一个。ConcurrentLinkedQueue 是基于链接节点的、线程安全的队列。并发访问不需要同步。因为它在队列的尾部添加元素并从头部删除它们,所以只要不需要知道队列的大小,ConcurrentLinkedQueue 对公共集合的共享访问就可以工作得很好。收集关于队列大小的信息会很慢,需要遍历队列。

使用阻塞队列
新的 java.util.concurrent 包在 Collection Framework 中可用的具体集合类中加入了 BlockingQueue 接口和五个阻塞队列类。假如不熟悉阻塞队列概念,它实质上就是一种带有一点扭曲的 FIFO 数据结构。不是立即从队列中添加或者删除元素,线程执行操作阻塞,直到有空间或者元素可用。BlockingQueue 接口的 Javadoc 给出了阻塞队列的基本用法,如清单 2 所示。生产者中的 put() 操作会在没有空间可用时阻塞,而消费者的 take() 操作会在队列中没有任何东西时阻塞。

清单 2. 使用 BlockingQueue
 

 

 class Producer implements Runnable {

   private final BlockingQueue queue;

   Producer(BlockingQueue q) { queue = q; }

   public void run() {

     try {

       while(true) { queue.put(produce()); }

     } catch (InterruptedException ex) { ... handle ...}

   }

   Object produce() { ... }

 }

 

 class Consumer implements Runnable {

   private final BlockingQueue queue;

   Consumer(BlockingQueue q) { queue = q; }

   public void run() {

     try {

       while(true) { consume(queue.take()); }

     } catch (InterruptedException ex) { ... handle ...}

   }

   void consume(Object x) { ... }

 }

 

 class Setup {

   void main() {

     BlockingQueue q = new SomeQueueImplementation();

     Producer p = new Producer(q);

     Consumer c1 = new Consumer(q);

     Consumer c2 = new Consumer(q);

     new Thread(p).start();

     new Thread(c1).start();

     new Thread(c2).start();

   }

 }

五个队列所提供的各有不同:

  • ArrayBlockingQueue:一个由数组支持的有界队列。
  • LinkedBlockingQueue:一个由链接节点支持的可选有界队列。
  • PriorityBlockingQueue:一个由优先级堆支持的无界优先级队列。
  • DelayQueue:一个由优先级堆支持的、基于时间的调度队列。
  • SynchronousQueue:一个利用 BlockingQueue 接口的简单聚集(rendezvous)机制。

前两个类 ArrayBlockingQueue LinkedBlockingQueue 几乎相同,只是在后备存储器方面有所不同,LinkedBlockingQueue 并不总是有容量界限。无大小界限的 LinkedBlockingQueue 类在添加元素时永远不会有阻塞队列的等待(至少在其中有 Integer.MAX_VALUE 元素之前不会)。

PriorityBlockingQueue 是具有无界限容量的队列,它利用所包含元素的 Comparable 排序顺序来以逻辑顺序维护元素。可以将它看作 TreeSet 的可能替代物。例如,在队列中加入字符串 OneTwoThree Four 会导致 Four 被第一个取出来。对于没有天然顺序的元素,可以为构造函数提供一个 Comparator 。不过对 PriorityBlockingQueue 有一个技巧。从 iterator() 返回的 Iterator 实例不需要以优先级顺序返回元素。如果必须以优先级顺序遍历所有元素,那么让它们都通过 toArray() 方法并自己对它们排序,像 Arrays.sort(pq.toArray())

新的 DelayQueue 实现可能是其中最有意思(也是最复杂)的一个。加入到队列中的元素必须实现新的 Delayed 接口(只有一个方法 —— long getDelay(java.util.concurrent.TimeUnit unit))。因为队列的大小没有界限,使得添加可以立即返回,但是在延迟时间过去之前,不能从队列中取出元素。如果多个元素完成了延迟,那么最早失效/失效时间最长的元素将第一个取出。实际上没有听上去这样复杂。清单 3 演示了这种新的阻塞队列集合的使用:

清单 3. 使用 DelayQueue 实现
 

 

import edu.emory.mathcs.backport..*;

import edu.emory.mathcs.backport..concurrent.*;

 

public class Delay {

  /**

   * Delayed implementation that actually delays

   */

  static class NanoDelay implements Delayed {

    long trigger;

    NanoDelay(long i) {

      trigger = Utils.nanoTime() + i;

    }

    public int compareTo(Object y) {

      long i = trigger;

      long j = ((NanoDelay)y).trigger;

      if (i < j) return -1;

      if (i > j) return 1;

      return 0;

    }

    public boolean equals(Object other) {

      return ((NanoDelay)other).trigger == trigger;

    }

    public boolean equals(NanoDelay other) {

      return ((NanoDelay)other).trigger == trigger;

    }

    public long getDelay(TimeUnit unit) {

      long n = trigger - Utils.nanoTime();

      return TimeUnit.NANOSECONDS.convert(n,unit);

    }

    public long getTriggerTime() {

      return trigger;

    }

    public String toString() {

      return String.valueOf(trigger);

    }

  }

  public static void main(String args[]) throws InterruptedException {

    Random random = new Random();

    DelayQueue queue = new DelayQueue();

    for (int i=0; i < 5; i++) {

      queue.add(new NanoDelay(random.nextInt(1000)));

    }

    long last = 0;

    for (int i=0; i < 5; i++) {

      NanoDelay delay = (NanoDelay)(queue.take());

      long tt = delay.getTriggerTime();

      System.out.println("Trigger time: " + tt);

      if (i != 0) {

        System.out.println("Delta: " + (tt - last));

      }

      last = tt;

    }

  }

}

这个例子首先是一个内部类 NanoDelay,它实质上将暂停给定的任意纳秒(nanosecond)数,这里利用了edu.emory.mathcs.backport.java.util.Utils nanoTime() 方法。然后 main() 方法只是将 NanoDelay 对象放到队列中并再次将它们取出来。如果希望队列项做一些其他事情,就需要在 Delayed 对象的实现中加入方法,并在从队列中取出后调用这个新方法。(请随意扩展 NanoDelay 以试验加入其他方法做一些有趣的事情。)显示从队列中取出元素的两次调用之间的时间差。如果时间差是负数,可以视为一个错误,因为永远不会在延迟时间结束后,在一个更早的触发时间从队列中取得项。

SynchronousQueue 类是最简单的。它没有内部容量。它就像线程之间的手递手机制。在队列中加入一个元素的生产者会等待另一个线程的消费者。当这个消费者出现时,这个元素就直接在消费者和生产者之间传递,永远不会加入到阻塞队列中。

清单4:使用DelayQueue实现生产者/消费者

package bjInfoTech.util.threadManage.concurrent.tryIt;

/*

 * DelayQueue_test.java

 *

 * Created on 2005111, 下午4:23

 */

import edu.emory.mathcs.backport.java.util.concurrent.*;

import java.util.*;

 

/**

 * 测试util.concurrent包的jsdk1.4-backport-util-concurrent---DelayQueue

 * @author 聪明的猪

 */

public class DelayQueue_test {

   

    private static DelayQueue Q=new DelayQueue();

   

    /**

     * 生产者:向队列中放入对象

     */

    private static class Producer implements Runnable {

        public void run() {

            while (true) {

                try {

                    Q.put(new Object());

                    Thread.yield();

                } catch (Exception e) {

                    Thread.currentThread().interrupt();

                }

            }

        }

    }

 

    /**

     * 消费者:从队列中取出50000个信息

     */

    private static class Consumer implements Runnable {

        private static final int MAX = 50000;

        /**

         *

         * @throws cassCastException 当生产者调用drainto方法处理传入object对象时,将发生该错误。

         */

        public void run() {

            long t1 = System.currentTimeMillis();

            List list = new ArrayList();

            int i = 0;

            while (i < MAX) {

                //i += Q.drainTo(list);

                //list.clear();

                i+=Q.size();

                Q.clear();

                Thread.yield();

            }

            long t2 = System.currentTimeMillis();

            System.out.println("time = " + (t2 - t1) + "ms");

        }

    }

 

    public static void main(String[] args) {

        Thread c = new Thread(new Consumer());

        c.start();

 

        Thread p = new Thread(new Producer());

        p.setDaemon(true);

        p.start();

    }

   

}

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值