现在的位置: 首页 > 综合 > 正文

ConcurrentHashMap之实现细节

2013年06月06日 ⁄ 综合 ⁄ 共 5944字 ⁄ 字号 评论关闭

ConcurrentHashMap是Java 5中支持高并发、高吞吐量的线程安全HashMap实现。在这之前我对ConcurrentHashMap只有一些肤浅的理解,仅知道它采用了多个锁,大概也足够了。但是在经过一次惨痛的面试经历之后,我觉得必须深入研究它的实现。面试中被问到读是否要加锁,因为读写会发生冲突,我说必须要加锁,我和面试官也因此发生了冲突,结果可想而知。还是闲话少说,通过仔细阅读源代码,现在总算理解ConcurrentHashMap实现机制了,其实现之精巧,令人叹服,与大家共享之。

 

 

实现原理 

 

锁分离 (Lock Stripping)

 

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

 

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

 

Java代码  收藏代码
  1. /** 
  2.  * The segments, each of which is a specialized hash table 
  3.  */  
  4. final Segment<K,V>[] segments;  

 

 

不变(Immutable)和易变(Volatile)

 

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

 

Java代码  收藏代码
  1. static final class HashEntry<K,V> {  
  2.     final K key;  
  3.     final int hash;  
  4.     volatile V value;  
  5.     final HashEntry<K,V> next;  
  6. }  

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

 


其它

 

为了加快定位段以及段中hash槽的速度,每个段hash槽的的个数都是2^n,这使得通过位运算就可以定位段和段中hash槽的位置。当并发级别为默认值16时,也就是段的个数,hash值的高4位决定分配在哪个段中。但是我们也不要忘记《算法导论》给我们的教训:hash槽的的个数不应该是2^n,这可能导致hash槽分配不均,这需要对hash值重新再hash一次。(这段似乎有点多余了 )

 

这是重新hash的算法,还比较复杂,我也懒得去理解了。

Java代码  收藏代码
  1. private static int hash(int h) {  
  2.     // Spread bits to regularize both segment and index locations,  
  3.     // using variant of single-word Wang/Jenkins hash.  
  4.     h += (h <<  15) ^ 0xffffcd7d;  
  5.     h ^= (h >>> 10);  
  6.     h += (h <<   3);  
  7.     h ^= (h >>>  6);  
  8.     h += (h <<   2) + (h << 14);  
  9.     return h ^ (h >>> 16);  
  10. }  

 

这是定位段的方法:

Java代码  收藏代码
  1. final Segment<K,V> segmentFor(int hash) {  
  2.     return segments[(hash >>> segmentShift) & segmentMask];  
  3. }  

 

 

 

数据结构

 

关于Hash表的基础数据结构,这里不想做过多的探讨。Hash表的一个很重要方面就是如何解决hash冲突,ConcurrentHashMap和HashMap使用相同的方式,都是将hash值相同的节点放在一个hash链中。与HashMap不同的是,ConcurrentHashMap使用多个子Hash表,也就是段(Segment)。下面是ConcurrentHashMap的数据成员:

 

Java代码  收藏代码
  1. public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>  
  2.         implements ConcurrentMap<K, V>, Serializable {  
  3.     /** 
  4.      * Mask value for indexing into segments. The upper bits of a 
  5.      * key's hash code are used to choose the segment. 
  6.      */  
  7.     final int segmentMask;  
  8.   
  9.     /** 
  10.      * Shift value for indexing within segments. 
  11.      */  
  12.     final int segmentShift;  
  13.   
  14.     /** 
  15.      * The segments, each of which is a specialized hash table 
  16.      */  
  17.     final Segment<K,V>[] segments;  
  18. }  

 

所有的成员都是final的,其中segmentMask和segmentShift主要是为了定位段,参见上面的segmentFor方法。

 

每个Segment相当于一个子Hash表,它的数据成员如下:

 

Java代码  收藏代码
  1.     static final class Segment<K,V> extends ReentrantLock implements Serializable {  
  2. private static final long serialVersionUID = 2249069246763182397L;  
  3.         /** 
  4.          * The number of elements in this segment's region. 
  5.          */  
  6.         transient volatile int count;  
  7.   
  8.         /** 
  9.          * Number of updates that alter the size of the table. This is 
  10.          * used during bulk-read methods to make sure they see a 
  11.          * consistent snapshot: If modCounts change during a traversal 
  12.          * of segments computing size or checking containsValue, then 
  13.          * we might have an inconsistent view of state so (usually) 
  14.          * must retry. 
  15.          */  
  16.         transient int modCount;  
  17.   
  18.         /** 
  19.          * The table is rehashed when its size exceeds this threshold. 
  20.          * (The value of this field is always <tt>(int)(capacity * 
  21.          * loadFactor)</tt>.) 
  22.          */  
  23.         transient int threshold;  
  24.   
  25.         /** 
  26.          * The per-segment table. 
  27.          */  
  28.         transient volatile HashEntry<K,V>[] table;  
  29.   
  30.         /** 
  31.          * The load factor for the hash table.  Even though this value 
  32.          * is same for all segments, it is replicated to avoid needing 
  33.          * links to outer object. 
  34.          * @serial 
  35.          */  
  36.         final float loadFactor;  
  37. }  

count用来统计该段数据的个数,它是volatile,它用来协调修改和读取操作,以保证读取操作能够读取到几乎最新的修改。协调方式是这样的,每次修改操作做了结构上的改变,如增加/删除节点(修改节点的值不算结构上的改变),都要写count值,每次读取操作开始都要读取count的值。这利用了Java 5中对volatile语义的增强,对同一个volatile变量的写和读存在happens-before关系。modCount统计段结构改变的次数,主要是为了检测对多个段进行遍历过程中某个段是否发生改变,在讲述跨段操作时会还会详述。threashold用来表示需要进行rehash的界限值。table数组存储段中节点,每个数组元素是个hash链,用HashEntry表示。table也是volatile,这使得能够读取到最新的table值而不需要同步。loadFactor表示负载因子。

 

 

实现细节

 

修改操作

 

先来看下删除操作remove(key)。

Java代码  收藏代码
  1. public V remove(Object key) {  
  2.  hash = hash(key.hashCode());  
  3.     return segmentFor(hash).remove(key, hash, null);  
  4. }  

整个操作是先定位到段,然后委托给段的remove操作。当多个删除操作并发进行时,只要它们所在的段不相同,它们就可以同时进行。下面是Segment的remove方法实现:

Java代码  收藏代码
  1. V remove(Object key, int hash, Object value) {  
  2.     lock();  
  3.     try {  
  4.         int c = count - 1;  
  5.         HashEntry<K,V>[] tab = table;  
  6.         int index = hash & (tab.length - 1);  
  7.         HashEntry<K,V> first = tab[index];  
  8.         HashEntry<K,V> e = first;  
  9.         while (e != null && (e.hash != hash || !key.equals(e.key)))  
  10.             e = e.next;  
  11.   
  12.         V oldValue = null;  
  13.         if (e != null) {  
  14.             V v = e.value;  
  15.             if (value == null || value.equals(v)) {  
  16.                 oldValue = v;  
  17.                 // All entries following removed node can stay  
  18.                 // in list, but all preceding ones need to be  
  19.                 // cloned.  
  20.                 ++modCount;  
  21.                 HashEntry<K,V> newFirst = e.next;  
  22.                 for (HashEntry<K,V> p = first; p != e; p = p.next)  
  23.                     newFirst = new HashEntry<K,V>(p.key, p.hash,  
  24.                                                   newFirst, p.value);  
  25.                 tab[index] = newFirst;  
  26.                 count = c; // write-volatile  
  27.             }  
  28.         }  
  29.         return oldValue;  
【上篇】
【下篇】

抱歉!评论已关闭.