Java并发集合类实例

原创 2010年06月07日 13:33:00

介绍 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()


基本上,一个队列就是一个先入先出(FIFO)的数据结构。一些队列有大小限制,因此如果想在一个满的队列中加入一个新项,多出的项就会被拒绝。这时新的 offer 方法就可以起作用了。它不是对调用 add() 方法抛出一个 unchecked 异常,而只是得到由 offer() 返回的 false。 remove()poll() 方法都是从队列中删除第一个元素(head)。 remove() 的行为与 Collection 接口的版本相似,但是新的 poll() 方法在用空集合调用时不是抛出异常,只是返回 null。因此新的方法更适合容易出现异常条件的情况。后两个方法 element()peek() 用于在队列的头部查询元素。与 remove() 方法类似,在队列为空时, element() 抛出一个异常,而 peek() 返回 null。





回页首



使用基本队列

在 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.AbstractListjava.util.AbstractSet 类。在创建自定义集合时,不用自己实现整个接口,只是继承抽象实现并填入细节。使用 AbstractQueue 时,必须为方法 offer()poll()peek() 提供实现。像 add()addAll() 这样的方法修改为使用 offer() ,而 clear()remove() 使用 poll() 。最后, element() 使用 peek() 。当然可以在子类中提供这些方法的优化实现,但是不是必须这么做。而且,不必创建自己的子类,可以使用几个内置的实现, 其中两个是不阻塞队列: PriorityQueueConcurrentLinkedQueue

PriorityQueueConcurrentLinkedQueue 类在 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)机制。

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

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

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



清单 3. 使用 DelayQueue 实现

import java.util.*;
import java.util.concurrent.*;
public class Delay {
  /**
   * Delayed implementation that actually delays
   */
  static class NanoDelay implements Delayed { 
    long trigger;
    NanoDelay(long i) { 
      trigger = System.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 - System.nanoTime();
      return unit.convert(n, TimeUnit.NANOSECONDS);
    }
    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)数,这里利用了 System 的新 nanoTime() 方法。然后 main() 方法只是将 NanoDelay 对象放到队列中并再次将它们取出来。如果希望队列项做一些其他事情,就需要在 Delayed 对象的实现中加入方法,并在从队列中取出后调用这个新方法。(请随意扩展 NanoDelay 以试验加入其他方法做一些有趣的事情。)显示从队列中取出元素的两次调用之间的时间差。如果时间差是负数,可以视为一个错误,因为永远不会在延迟时间结束后,在一个更早的触发时间从队列中取得项。

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





回页首



使用 ConcurrentMap 实现

新的 java.util.concurrent.ConcurrentMap 接口和 ConcurrentHashMap 实现只能在键不存在时将元素加入到 map 中,只有在键存在并映射到特定值时才能从 map 中删除一个元素。

有一个新的 putIfAbsent() 方法用于在 map 中进行添加。这个方法以要添加到 ConcurrentMap 实现中的键的值为参数,就像普通的 put() 方法,但是只有在 map 不包含这个键时,才能将键加入到 map 中。如果 map 已经包含这个键,那么这个键的现有值就会保留。 putIfAbsent() 方法是原子的。如果不调用这个原子操作,就需要从适当的同步块中调用清单 4 中的代码:



清单 4. 等价的 putIfAbsent() 代码

  if (!map.containsKey(key)) {
    return map.put(key, value);
  } else {
    return map.get(key);
  }


putIfAbsent() 方法一样,重载后的 remove() 方法有两个参数 —— 键和值。在调用时,只有当键映射到指定的值时才从 map 中删除这个键。如果不匹配,那么就不删除这个键,并返回 false。如果值匹配键的当前映射内容,那么就删除这个键。清单 5 显示了这种操作的等价源代码:



清单 5. 等价的 remove() 代码

  if (map.get(key).equals(value)) {
    map.remove(key);
    return true;
  } else {
    return false;
  }






回页首



使用 CopyOnWriteArrayList 和 CopyOnWriteArraySet

在 Doug Lea 的 Concurrent Programming in Java一书的第 2 章第 2.4.4 节(请参阅 参考资料)中,对 copy-on-write 模式作了最好的描述。实质上,这个模式声明了,为了维护对象的一致性快照,要依靠不可变性(immutability)来消除在协调读取不同的但是相关的属性时需要的同步。对于集合,这意味着如果有大量的读(即 get() ) 和迭代,不必同步操作以照顾偶尔的写(即 add() )调用。对于新的 CopyOnWriteArrayListCopyOnWriteArraySet 类,所有可变的(mutable)操作都首先取得后台数组的副本,对副本进行更改,然后替换副本。这种做法保证了在遍历自身更改的集合时,永远不会抛出 ConcurrentModificationException 。遍历集合会用原来的集合完成,而在以后的操作中使用更新后的集合。

这些新的集合, CopyOnWriteArrayListCopyOnWriteArraySet ,最适合于读操作通常大大超过写操作的情况。一个最常提到的例子是使用监听器列表。已经说过,Swing 组件还没有改为使用新的集合。相反,它们继续使用 javax.swing.event.EventListenerList 来维护它们的监听器列表。

如清单 6 所示,集合的使用与它们的非 copy-on-write 替代物完全一样。只是创建集合并在其中加入或者删除元素。即使对象加入到了集合中,原来的 Iterator 也可以进行,继续遍历原来集合中的项。



清单 6. 展示一个 copy-on-write 集合

import java.util.*;
import java.util.concurrent.*;
public class CopyOnWrite {
  public static void main(String args[]) {
    List list1 = new CopyOnWriteArrayList(Arrays.asList(args));
    List list2 = new ArrayList(Arrays.asList(args));
    Iterator itor1 = list1.iterator();
    Iterator itor2 = list2.iterator();
    list1.add("New");
    list2.add("New");
    try {
      printAll(itor1);
    } catch (ConcurrentModificationException e) {
      System.err.println("Shouldn't get here");
    }
    try {
      printAll(itor2);
    } catch (ConcurrentModificationException e) {
      System.err.println("Will get here.");
    }
  }
  private static void printAll(Iterator itor) {
    while (itor.hasNext()) {
      System.out.println(itor.next());
    }
  }
}


这个示例程序用命令行参数创建 CopyOnWriteArrayListArrayList 这两个实例。在得到每一个实例的 Iterator 后,分别在其中加入一个元素。当 ArrayList 迭代因一个 ConcurrentModificationException 问题而立即停止时, CopyOnWriteArrayList 迭代可以继续,不会抛出异常,因为原来的集合是在得到 iterator 之后改变的。如果这种行为(比如通知原来一组事件监听器中的所有元素)是您需要的,那么最好使用 copy-on-write 集合。如果不使用的话,就还用原来的,并保证在出现异常时对它进行处理。

java中的并发集合类概述

一,普通集合类中线程安全的集合:Vector  && Stack 1,Vector: (1)默认分配初始大小为10的数组,在构造方法中可以指定数组初始大小和增长大小。 (2)线程安全主要是通过在...
  • xiaye1989
  • xiaye1989
  • 2015-11-23 20:08:58
  • 2474

Java 并发编程(九)并发集合框架

集合框架简介编程中,我们经常需要集中存放多个数据。数组是我们的一个很好的选择,前提是我们事先明确我们将要保存对象的数量。数组在初始化时如果指定了长度,那这个数组长度就是不可变的了,如果我们需要保存一个...
  • hongguo_cheng
  • hongguo_cheng
  • 2017-10-18 19:59:31
  • 782

java 常用并发类

1.ConcurrentHashMap是HashMap的线程安全类。 2.CopyOnWriteArrayList是一个线程安全、并在读操作时无锁的ArrayList。 3.CopyOnWrite...
  • u012209513
  • u012209513
  • 2017-02-17 23:53:33
  • 800

java并发大总结

引子: 在一个list中有过亿条的Integer类型的值,如何更快的计算这些值的总和? 要学习的东西那么多!扶我起来。。 慢慢来深入,慢慢来领略大牛的思想融汇。 注:本文汇集各方材料而成,...
  • basycia
  • basycia
  • 2016-07-28 18:31:04
  • 3919

Java并发工具类详解

Java并发工具类详解,包括CountDownLatch、CyclicBarrier、Semaphore、Exchanger
  • sunxianghuang
  • sunxianghuang
  • 2016-08-22 20:23:19
  • 2584

关于Java并发编程的总结和思考

编写优质的并发代码是一件难度极高的事情。Java语言从第一版本开始内置了对多线程的支持,这一点在当年是非常了不起的,但是当我们对并发编程有了更深刻的认识和更多的实践后,实现并发编程就有了更多的方案和更...
  • jackfrued
  • jackfrued
  • 2015-03-27 10:36:10
  • 101842

Java高并发编程:原子类

并发编程概念1. 原子性一个操作不能被再拆分了;即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。一个很经典的例子就是银行账户转账问题。 增量操作符++,不是原子...
  • axi295309066
  • axi295309066
  • 2016-10-25 00:26:03
  • 1717

Java中的并发集合类

在Java类库中出现的第一个关联的集合类是Hashtable,它是JDK1.0的一部分。Hashtable提供了一种易于使用的、线程安全的、关联的map功能,这当然也是方便的。然而,线程安全性是凭代价...
  • guomei
  • guomei
  • 2013-08-20 01:38:53
  • 1226

java 并发的例子-并行数据处理

JDK5引进了一个高级并发的API,它位于java.util.concurrent这个包中。 它考虑到了更优雅和直观的多线程编程。我知道这对有些人来说并不算什么新消息,但是我发现 大部分程序员仍...
  • sunnyboy9
  • sunnyboy9
  • 2017-04-19 23:57:17
  • 1527

java 高并发操作实例-秒杀

业务大致流程: 系统时间获取 1. 原因 1.1 用户会大量刷新页面 1.2 CDN存放的是大量的静态页面,静态css,js资源 Tips:CDN的理解:内容分发网络 1.2.1 ...
  • qq_25797077
  • qq_25797077
  • 2016-06-26 23:50:48
  • 14464
收藏助手
不良信息举报
您举报文章:Java并发集合类实例
举报原因:
原因补充:

(最多只允许输入30个字)