Java Concurrent Programming (6)

6. 并发集合类
Hashtable是一个易于使用,线程安全的集合类,它的线程安全性是凭借代价换来的--Hashtable中的所有方法都是同步的。HashMap是Hashtable的继承者,通过一个不同步的基类和一个同步的包装器Collections.synchronizedMap解决了线程安全性问题。通过将基本的功能从线程安全性中分离开来,Collections.synchronizedMap允许需要同步的用户可以拥有同步,而不需要同步的用户不需要为同步付出代价。Hashtable和Collections.synchronizedMap通过同步每个方法获得线程安全。这意味着当一个线程执行一个 Map 方法时,无论其他线程要对 Map 进行什么样操作,都不能执行,直到第一个线程结束才可以。 这样,就会有两个不足之处:首先,可伸缩性差,因为一次只能有一个线程可以访问hash表。其次,许多公用的混合操作仍需要外部同步,比如put-if-absent操作,必须要外部同步来保证数据的争用。java.util.concurrent 包添加了多个新的线程安全集合类(ConcurrentHashMap、CopyOnWriteArrayList 和 CopyOnWriteArraySet)。这些类的目的是提供高性能、高度可伸缩性、线程安全的基本集合类型版本。

6.1 ConcurrentHashMap类
ConcurrentHashMap类是对 Map 的线程安全的实现,比起 synchronizedMap 来,它提供了好得多的并发性。多个读操作几乎总可以并发地执行,ConcurrentHashMap的读操作并不需要加锁,是因为其HashEntry被定义为final(HashEntry代表每个hash链中的一个节点),而value被修饰为volatile的原因:

static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
}

ConcurrentHashMap允许多个修改操作并发的执行,是因为其使用了锁分离技术,也就是说,它用多个锁来控制对hash表不同部分的修改操作。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hash table,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行,其也被定义为final,保证了数据的不变性。

final Segment[] segments;

每当进行修改和删除的时候,首先判断是否在同一段内,只要不在同一段内,都可以进行并发的操作。

public V put(K key, V value) {
if (value == null)
throw new NullPointerException();
int hash = hash(key);
return segmentFor(hash).put(key, hash, value, false);
}
public V remove(Object key) {
int hash = hash(key);
return segmentFor(hash).remove(key, hash, null);
}

ConcurrentHashMap返回的迭代器是弱一致的,并且返回的Iterators将每次最多返回一个元素,意味着它们将不抛出 ConcurrentModificationException。

final class KeyIterator extends HashIterator implements Iterator<K>, Enumeration<K>
final class ValueIterator extends HashIterator implements Iterator<V>, Enumeration<V>
final class EntryIterator extends HashIterator implements Map.Entry<K,V>,Iterator<Entry<K,V>>


6.2 CopyOnWriteArrayList和CopyOnWriteArraySet
CopyOnWriteArrayList和CopyOnWriteArraySet,适合于读操作通常大大超过写操作的情况。在CopyOnWriteArrayList 和 CopyOnWriteArraySet类中,所有可变的(mutable)操作都首先取得后台数组的副本,对副本进行更改,然后替换副本。这种做法保证了在遍历自身更改的集合时,永远不会抛出 ConcurrentModificationException 。遍历集合会用原来的集合完成,而在以后的操作中使用更新后的集合。

6.3 Queue与BlockingQueue
JDK 5.0 还提供了两个新集合接口 —— Queue 和 BlockingQueue。Queue继承自Collection,与 List 类似,但它只允许从后面插入,从前面删除。通过消除 List 的随机访问要求,可以创建比现有 ArrayList 和 LinkedList 实现性能更好的 Queue 实现。因为 List 的许多应用程序实际上不需要随机访问,所以Queue 通常可以替代 List,来获得更好的性能。其定义和方法如下:

public interface Queue<E> extends Collection<E> {
boolean offer(E o);
E poll();
E remove();
E peek();
E element();
}

一个队列就是一个先进先出FIFO的数据结构。队列有大小的限制,因此如果想在一个满的队列中继续添加元素,队列会拒绝次元素。如果此时调用的是add方法,那么会抛出异常。队里中的offer方法表示,如果向已满队列中继续添加元素,它会返回false。poll和remove方法都是从队列中删除第一个元素(head)。区别在于,如果队列为空,remove会抛出异常,而poll会返回null。peek和element用于在队列的头部查询元素,与remove方法相似,如果队列为空时,element抛出异常,peek返回null。
再复杂一点的是新的 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实现来定位。ConcurrentLinkedQueue是基于链接节点的、线程安全的队列。并发访问不需要同步。因为它在队列的尾部添加元素并从头部删除它们,所以只要不需要知道队列的大小,ConcurrentLinkedQueue对公共集合的共享访问就可以工作得很好。收集关于队列大小的信息会很慢,需要遍历队列。

BlockingQueue继承自Queue,添加了两个操作:获取元素时等待队列变为非空,以及存储元素时等待空间变得可用。其定义和新添加方法如下:

public interface BlockingQueue<E> extends Queue<E> {
E take() throws InterruptedException;
void put(E o) throws InterruptedException;
int drainTo(Collection<? super E> c);
int drainTo(Collection<? super E> c, int maxElements);
}

take表示当删除队列中第一个元素时,如果队列为空,会一直等待队列变为非空。put表示向已满队列添加元素,会一直等待队列空间变得可用。drainTo表示将队列中的元素添加到指定集合中,返回添加的个数。利用take和put方法,实现生产者和消费者:

class Producer implements Runnable {

private final BlockingQueue queue;

public Producer(BlockingQueue q) {
queue = q;
}

public void run() {
try {
while (true) {
queue.put(produce());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

private Object produce() {
return null;
}
}

class Consumer implements Runnable {

private final BlockingQueue queue;

public Consumer(BlockingQueue q) {
queue = q;
}

public void run() {
try {
while (true) {
consume(queue.take());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

private void consume(Object x) {
//
}
}

BlockingQueue的5个实现类分别为:

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

ArrayBlockingQueue和LinkedBlockingQueue几乎相同,只是在后备存储器方面有所不同,ArrayBlockingQueue为数组存储,LinkedBlockingQueue为链接节点存储,LinkedBlockingQueue中的头元素为在队列中时间最长的元素。LinkedBlockingQueue 并不总是有容量界限。无大小界限的 LinkedBlockingQueue 类在添加元素时永远不会有阻塞队列的等待(至少在其中有 Integer.MAX_VALUE 元素之前不会)。
PriorityBlockingQueue是具有无界限容量的队列,它利用所包含元素的Comparable排序顺序来以逻辑顺序维护元素。可以将它看作TreeSet的可能替代物。例如,在队列中加入字符串One,Two,Three和Four会导致Four被第一个取出来。对于没有天然顺序的元素,可以为构造函数提供一个Comparator 。不过对PriorityBlockingQueue有一个技巧。从iterator()返回的Iterator实例不需要以优先级顺序返回元素。如果必须以优先级顺序遍历所有元素,那么让它们都通过 toArray()方法并自己对它们排序,像 Arrays.sort(pq.toArray()) 。
DelayQueue的定义如下,所以像其中添加的元素必须实现Delay接口(只有一个方法long getDelay(TimeUnit unit)):

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E>

因为队列的大小没有界限,使得添加可以立即返回,但是在延迟时间过去之前,不能从队列中取出元素。如果多个元素完成了延迟,那么最早失效/失效时间最长的元素将第一个取出。
SynchronousQueue 类是最简单的。它没有内部容量。它就像线程之间的手递手机制。在队列中加入一个元素的生产者会等待另一个线程的消费者。当这个消费者出现时,这个元素就直接在消费者和生产者之间传递,永远不会加入到阻塞队列中。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值