ConcurrentHashMap学习

来源:互联网 发布:融资毕业论文知乎 编辑:程序博客网 时间:2024/09/21 09:22

ConcurrentHashMap

原文章:http://www.iteye.com/topic/344876

实现原理:

锁分离 (Lock Stripping)

ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用(Segment)来表示这些不同的部分,每个段其实就是一个小的hash table,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。

<span style="white-space:pre"></span>final Segment<K,V>[] segments; 

有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。不变性是多线程编程占有很重要的地位,下面还要谈到

不变(Immutable)和易变(Volatile)

ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。如果使用传统的技术,如HashMap中的实现,如果允许可以在hash链的中间添加或删除元素,读操作不加锁将得到不一致的数据。ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。HashEntry代表每个hash链中的一个节点,其结构如下所示:

    static final class HashEntry<K,V> {          final K key;          final int hash;          volatile V value;          final HashEntry<K,V> next;      }  
可以看到除了value不是final的,其它值都是final的,这意味着不能从hash链的中间或尾部添加或删除节点,因为这需要修改next引用值,所有的节点的修改只能从头部开始。对于put操作,可以一律添加到Hash链的头部。但是对于remove操作,可能需要从中间删除一个节点,这就需要将要删除节点的前面所有节点整个复制一遍,最后一个节点指向要删除结点的下一个结点。这在讲解删除操作时还会详述。为了确保读操作能够看到最新的值,将value设置成volatile,这避免了加锁。

数据结构

Hash表的一个很重要方面就是如何解决hash冲突,ConcurrentHashMap和HashMap使用相同的方式,都是将hash值相同的节点放在一个hash链中。与HashMap不同的是,ConcurrentHashMap使用多个子Hash表,也就是段(Segment)。下面是ConcurrentHashMap的数据成员:
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>        implements ConcurrentMap<K, V>, Serializable {    private static final long serialVersionUID = 7249069246763182397L;
    /**     * Mask value for indexing into segments. The upper bits of a     * key's hash code are used to choose the segment.     */    final int segmentMask;
    /**     * Shift value for indexing within segments.     */    final int segmentShift;    /**     * The segments, each of which is a specialized hash table.     */    final Segment<K,V>[] segments;
final的成员,其中segmentMask和segmentShift主要是为了定位段,参见上面的segmentFor方法。每个Segment相当于一个子Hash表,它的数据成员如下:
static final class Segment<K,V> extends ReentrantLock implements Serializable {
<span style="white-space:pre"></span>/**         * The per-segment table. Elements are accessed via         * entryAt/setEntryAt providing volatile semantics.         */        transient volatile HashEntry<K,V>[] table;
<span style="white-space:pre"></span>/**         * The number of elements. Accessed only either within locks         * or among other volatile reads that maintain visibility.         */        transient int count;
<pre name="code" class="java">        /**         * The total number of mutative operations in this segment.         * Even though this may overflows 32 bits, it provides         * sufficient accuracy for stability checks in CHM isEmpty()         * and size() methods.  Accessed only either within locks or         * among other volatile reads that maintain visibility.         */        transient int modCount;
        /**         * The table is rehashed when its size exceeds this threshold.         * (The value of this field is always <tt>(int)(capacity *         * loadFactor)</tt>.)         */        transient int threshold;
        /**         * The load factor for the hash table.  Even though this value         * is same for all segments, it is replicated to avoid needing         * links to outer object.         * @serial         */        final float loadFactor;
count用来统计该段数据的个数,它是volatile,它用来协调修改和读取操作,以保证读取操作能够读取到几乎最新的修改。
协调方式是这样的,每次修改操作做了结构上的改变,如增加/删除节点(修改节点的值不算结构上的改变),都要写count值,每次读取操作开始都要读取count的值。这利用了Java 5中对volatile语义的增强,对同一个volatile变量的写和读存在happens-before关系。modCount统计段结构改变的次数,主要是为了检测对多个段进行遍历过程中某个段是否发生改变,在讲述跨段操作时会还会详述。threashold用来表示需要进行rehash的界限值。table数组存储段中节点,每个数组元素是个hash链,用HashEntry表示。table也是volatile,这使得能够读取到最新的table值而不需要同步。loadFactor表示负载因子






0 0