前言
ConcurrentHashMap是java.util.concurrent包下的一个类,它设计出来是用来在某些情况下替换Hashtable的。相比Hashtable它能够更加高效的进行多线程操作,并不一定需要像Hashtable一样,当一个线程占有锁的时候其他的线程都必须进入阻塞状态,因此在多线程环境下它更加的高效。至于,ConcurrentHashMap是否能够完全替代Hashtable这个问题,博文中有分析到:https://my.oschina.net/hosee/blog/675423。但是,同时它也降低了对数据一致性的要求。在这里额外提一下,java.util.concurrent包中的并发容器,设计出来是用来替换同步容器(多线程环境下,一个线程占有锁的时候其他线程必需进入等待状态,比如Hashtable,Vector),以提供更加高效的并发。
本文将基于JDK1.7的源码进行分析,JDK1.8之后再写。
设计思路
CocurrentHashMap使用分段锁(segment)的方式来减少锁的粒度,它有一个Segment[]属性。不同的线程访问不同segment里面的数据不会产生阻塞,只有多个线程访问同一个segment才会产生锁的竞争。segment中有一个HashEntry数组,同时它还继承了ReentrantLock,所以它就类似于一个Hashtable。
/**
* The segments, each of which is a specialized hash table.
*/
final Segment<K,V>[] segments;
static final class Segment<K,V> extends ReentrantLock implements Serializable
transient volatile HashEntry<K,V>[] table;
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
HashEntry节点和HashMap中的Entry稍有不同的就是,value和next节点都有volatile关键词修饰,这个是为了保证多线程环境下的可见性。ConcurrentHashMap的结构图大致如下:
Hash算法
无论是HashMap还是ConcurrentHashMap的基础都是hash算法,下面是它的hash算法相关的源码:
private int hash(Object k) {
int h = hashSeed;
if ((0 != h) && (k instanceof String)) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// Spread bits to regularize both segment and index locations,
// using variant of single-word Wang/Jenkins hash.
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
可能有人会有疑问,为什么能够直接通过Object的hashcode()方法得到hashcode的值之后还需要再单独定义一个hash()方法进行再哈希?因为直接计算索引受限于segments.length转换为2进制后的有效位数。具体看下面这个例子体会一下:
对于segments.length == 16的ConcurrentHashMap,(segments.length - 1) == 15转换为2进制数后为1111,有效位数为4位。现有4个key,hascode的值分别为:15,31,63,127,它们转换为2进制数后对应的值分别为:1111,11111,111111,111111。现在对它们求索引:
15 & 15 ==> 1111 & 1111 = 1111
31 & 15 ==> 11111 & 01111 = 01111
63 & 15 ==> 111111 & 001111 = 001111
127 & 15 ==> 1111111 & 0001111 = 0001111
最终求出索引的值转换为10进制数后都是15,可以看出直接用hascode的值求索引,受限于length的转换为2进制的有效位数,比较容易产生hash冲突。为了解决这个问题,就需要利用key的hascode转换为2进制的后的有效位数的不同,进行再hash运算,最终使得进行&运算的时候有效位数不同。仍然是上面的这个例子,看一下通过Wang/Jenkins hash算法之后,为了方便阅读将数据转换为32位的2进制数据,不足位用0补齐:
0100 0111 0110 0111 1101 1010 0100 1110
1111 0111 0100