Java并发编程实战-第5章 基础构建模块- 高飞网

第5章 基础构建模块

——同步容器类

2016-04-02 21:48:00.0

    同步容器类包括Vector和Hashtable,此外还有通过Collections.synchronizedXxx等工厂方法创建的。这些类实现线程安全的方式是:将它们的状态封装起来,并对每个仅有方法都进行同步,使得每次只有一个线程能访问容器的状态。(因此这种类的性能很低

5.1 同步容器类

5.1.1 同步类容器的问题

    同步容器类都是线程安全的,但在某些情况下可能需要额外的客户端加锁来保护复合操作。这类复合操作包括迭代、跳转以及有条件运算。如:

import java.util.*; 
public class SynCollection{ 
    public static Object getLast(Vector list){ 
        int lastIndex = list.size() - 1 ; 
        return list.get(lastIndex); 
    }   
 
    public static void deleteLast(Vector list){ 
        int lastIndex = list.size() - 1 ; 
        list.remove(lastIndex); 
    }   
}

    这样在计算lastIndex完成之后,其他线程如果删除了最后一个元素,则后面的操作get(lastIndex)和remove(lastIndex)就会报ArrayIndexOutOfBoundException异常。如下在客户端进行加锁控制就可以解决以上问题。

import java.util.*;
public class SynCollection2{
    public static Object getLast(Vector list){
        synchronized(list){
            int lastIndex = list.size() - 1 ; 
            return list.get(lastIndex);
        }   
    }   

    public static void deleteLast(Vector list){
        synchronized(list){
            int lastIndex = list.size() - 1 ; 
            list.remove(lastIndex);
        }   
    }   
}

同样,迭代也是这类问题。

5.1.2 迭代器与ConcurrentModificationException

    如果集合在迭代期间被修改了,则在调用hasNext或next的时候,将抛出ConcurrentModificationException。此处采用了“快速失败”的问题。快速失败的实现方式是,将计数器的变化与容器关联起来:如果在迭代期间计数器被修改,那么hasNext或next将抛出ConcurrentModificationException。

    使用for-each语法对集合进行迭代,放在字节码层面来看,也是对Interator反复的进行hasNext或next操作。

    解决办法首先是对迭代器加锁,以保证在迭代期间没有线程对容器修改,但这样会大大降低吞吐量和CPU的利用率,尤其在容器非常大的时候。另外一个办法是复制一个容器对象,在副本上进行迭代操作。但复制操作本身也可能消耗很大。

    Java提高篇(三四)-----fail-fast机制

5.2 并发容器

    并发容器是针对多个线程并发访问设计的。Java5.0中增加了ConcurrentHashMap,用来替代同步且基于散列的Map,以及CopyOnWriteArrayList,用于在遍历操作作为主要操作的情况下代替同步的List。Java5.0增加了两种新的容器类型:Queue和BlockingQueue。Queue用来临时保存一组等待处理的元素。它提供了几种实现,包括:ConcurrentLinkedQueue、PriorityQueue(非并发的优先队列)。Queue上的操作不会阻塞,如果列表为空,那么获取元素的操作将返回空值。

    BlockingQueue扩展了Queue,增加了可阻塞的插入和获取等操作。如果队列为空,那么获取元素的操作将一直阻塞,直到队列中出现一个可用的元素。如果列表已满(对有界队列来说),插入元素的操作将一直阻塞,直到队列中出现可用的空间。

5.2.1 ConcurrentHashMap

    同步类容器在执行每个操作期间都持有一个锁,将使并发性大大降低。而ConcurrentHashMap是一种完全不同的加锁策略,可提供更高的并发性的伸缩性。ConcurrentHashMap并不是将每个方法都在同一个锁上同步并使得每次只能一个线程访问容器,而是使用一种粒度更细的加锁机制来实现更大程度的共享,这种机制称为分段锁(Lock Strping),在这种机制中,任意数量的读取线程可以并发地访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发地修改Map,ConcurrentHhashMap带来的结果是,在并发访问环境下将实现更高的吞吐量,而在单线程环境中只损失非常小的性能。

    对于一黄河旋风需要在整个Map上进行计数的方法,如size和isEmpty,这些方法的语义被略微减弱了以反映容器的并发特性。由于size的返回结果在计算时可能已经过期了,它实际上只是一个估计值,因此允许size返回一个近似值而不是一个精确值。

5.2.2 额外的原子Map操作

    由于ConcurrentHashMap不能被加锁来执行独立访问,因此无法使用客户端加锁来创建新的原子操作。

public interface ConcurrentMap<K, V> extends Map<K, V> {
    /**
     * If the specified key is not already associated
     * with a value, associate it with the given value.
     * This is equivalent to
     * <pre>
     *   if (!map.containsKey(key))
     *       return map.put(key, value);
     *   else
     *       return map.get(key);</pre>
     * except that the action is performed atomically.
     */
    V putIfAbsent(K key, V value);

    /**
     * Removes the entry for a key only if currently mapped to a given value.
     * This is equivalent to
     * <pre>
     *   if (map.containsKey(key) &amp;&amp; map.get(key).equals(value)) {
     *       map.remove(key);
     *       return true;
     *   } else return false;</pre>
     * except that the action is performed atomically.
     */
    boolean remove(Object key, Object value);

    /**
     * Replaces the entry for a key only if currently mapped to a given value.
     * This is equivalent to
     * <pre>
     *   if (map.containsKey(key) &amp;&amp; map.get(key).equals(oldValue)) {
     *       map.put(key, newValue);
     *       return true;
     *   } else return false;</pre>
     * except that the action is performed atomically.
     */
    boolean replace(K key, V oldValue, V newValue);

    /**
     * Replaces the entry for a key only if currently mapped to some value.
     * This is equivalent to
     * <pre>
     *   if (map.containsKey(key)) {
     *       return map.put(key, value);
     *   } else return null;</pre>
     * except that the action is performed atomically.
     */
    V replace(K key, V value);
}

5.2.3 CopyOnWriteArrayList

    CopyOnWriteArrayList用于替代同步List,在某些情况下它提供了更好的并发性能,并且在迭代期间不需要对容器进行加锁或复制(因为操作的数组永远不会发生变化)。

    写入时复制(Copy-On-Write)容器的线程安全在于,只要正确地发布一个事实不可变的对象,那么在访问该对象时就不再需要进一步的同步。在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变性。“写入时复制”容器的迭代器保留一个指向底层基础数组的引用,这个数组当前位于迭代器的起始位置,由于它不会被修改,因此在对其进行同时时只需确保数组内的可见性。因此多个线程可以同时对这个容器进行迭代,而不会彼此干扰或与修改容器的线程相互干扰。

    当迭代操作远远大于修改操作时,才应该使用“写入时复制”容器。

CopyOnWriteArrayList详解

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

    如果队列已满,put将阻塞直到有空间可用;add将抛出异常;offer方法将返回false。

    如果队列为空,take方法将阻塞直到有元素可用;remove将抛出异常;poll将返回空。

    BlockingQueue,阻塞队列,实现有LinkedBlockingQueue和ArrayBlockingQueue是FIFO队列;PriorityBlockingQueue是一个按优先级排序的队列,可以按元素的自然顺序来比较元素。

    另外还有SynchronousQueue,Java并发包中的同步队列SynchronousQueue实现原理