ConcurrentHashMap实现原理

    HashMap是很常用的集合,在并发环境中,对一个集合的多线程操作,需要加锁来保证一致性。常出现两个或多个线程同时访问同一个临界资源,而导致线程的等待,使cpu的使用率降低。这里的 ConcurrentHashMap是一个并发集合的设计,设计的目的是为了保持集合在多线程的环境下的读效率,同时其他操作的效率也比使用HashMap的效率要有所提高。
    HashTable的实现是对整个集合的加锁操作来保证一致性,而ConcurrentHashMap允许多个操作并发进行,原理在于使用了锁分离的设计。它将整个集合划分成了多个小集合。每个小集合就像一个HashTable,对操作加锁。所以如果多个操作的对象位于不同的小集合中,这些操作就不会产生锁竞争,从而提升了并发效率。但作为一个集合,有一些操作是对整个集合为目标的,比如size()等。这些操作涉及到了跨小集合,ConcurrentHashMap首先以乐观的方式尝试不加锁进行,如果失败了,再对所有的小集合有顺序的加锁后,实现操作。
    ConcurrentHashMap做的还不止这些,它还实现了get()操作在多线程环境下也不用加任何锁!!!主要是通过jdk1.5后的valatile的特性,实现了这一功能。不过,这种实现不能提供很强的一致性,效率和准确总要有所牺牲,看使用者的追求了。如果你的系统非常严谨,需要很强的一致性,可以自己对实现进行定制或使用其他方案。
    下面是JDK1.7中的实现代码片段:
final Segment<K,V>[] segments;   //Segment即是小集合,这个数组存储了所有的小集合
    Segment:
transient volatile int count;  //总数,volatile用来协调读写操作,保证写入的值能立即读取到
transient int modCount; //修改次数,检测对多个小集合的遍历过程中,是否有集合发生了改变
transient int threshold; //需要rehash的界限
transient volatile HashEntry<K,V>[] table; //元素数组,类型为HashEntry

    Segment中,每个真正元素的类型为HashEntry:
static final class HashEntry<K,V> {
final K key; //key和hash都是final的,一旦创建就不能修改,这保证了一致性
final int hash;
volatile V value; //value用volatile修饰,保证了value的修改立即对其他线程可见
final HashEntry<K,V> next; //next也是final的,同样保证一致性,小集合的结构如果修改,需要对相关的节点重建
}
    通过next元素可以看出, ConcurrentHashMap采用了HashMap一样的方式:用链表处理hash冲突。
    下面看一些主要方法:
final Segment<K,V> segmentFor(int hash) {
return segments[(hash >>> segmentShift) & segmentMask];
}
    定位一个元素属于哪个子集合,集合数量始终为2^n(默认16),所以用位运算,定位很快。
    Get操作:
V get(Object key, int hash) {
if (count != 0) { // read-volatile
HashEntry<K,V> e = getFirst(hash);//得到头节点
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null) return v;
return readValueUnderLock(e);
}
e = e.next;
}
}
return null;
}
    这里做的第一件事就是去count变量,因为结构更新操作的最后一步都是写count变量,这可以让get接下来的getFirst获取到最新的结构。如果结构没有变化,而是某个元素的value变化了,因为value也是volatile的,所以也可以读到最新值。因此get方法,不用加锁也保证了一致性。但这种一致性是不完美的,因为如果在getFirst获取了某个链的头节点后,其他操作对这个链进行了删除或者更新,这里是拿不到最新值的,因为上面解释过,HashEntry中的next是fianl的,导致结构更新需要重建链表。所以这时,我们getFirst取到的链就已经是旧数据了。但还是那句话:总要有所取舍。对一致性不是那么苛刻的应用来说,已经够用了。看一下remove操作来理解刚才说的链更新问题,就明白为什么可能不是最新的了。
public V remove(Object key) {
int hash = hash(key.hashCode());
return segmentFor(hash).remove(key, hash, null); //委托给小集合执行
}
    看 segment的remove:
V remove(Object key, int hash, Object value) {
lock();
try {
int c = count - 1;
HashEntry<K,V>[] tab = table;
int index = hash & (tab.length - 1);
HashEntry<K,V> first = tab[index]; //找到hash值对应的链表
HashEntry<K,V> e = first;
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next; //找到最终目标

V oldValue = null;
if (e != null) {
V v = e.value;
if (value == null || value.equals(v)) {
oldValue = v;
// All entries following removed node can stay
// in list, but all preceding ones need to be
// cloned.
++modCount; //增加修改次数
HashEntry<K,V> newFirst = e.next; //next是final的,所以目标节点之前的元素全都需要复制一遍
for (HashEntry<K,V> p = first; p != e; p = p.next)//复制出的新节点加入到链表中,旧元素被抛弃掉了
newFirst = new HashEntry<K,V>(p.key, p.hash,//如果我们get中的getFirst操作比这一段操作先发生,然后执行了这一段
newFirst, p.value);//那getFirst后续操作,将对一些被抛弃的旧元素操作
tab[index] = newFirst;
count = c; // write-volatile
}
}
return oldValue;
} finally {
unlock();
}
}
在看一个跨子集合的操作:
public int size() {
final Segment<K,V>[] segments = this.segments;
long sum = 0;
long check = 0;
int[] mc = new int[segments.length];
for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) { //不加锁的情况下尝试RETRIES_BEFORE_LOCK次
check = 0;
sum = 0;
int mcsum = 0;
for (int i = 0; i < segments.length; ++i) { //算出结果和当前的修改次数
sum += segments[i].count;
mcsum += mc[i] = segments[i].modCount;
}
if (mcsum != 0) { //再次计算
for (int i = 0; i < segments.length; ++i) {
check += segments[i].count;
if (mc[i] != segments[i].modCount) { //如果某个结构变化了,尝试失败
check = -1; // force retry
break;
}
}
}
if (check == sum) //尝试成功
break;
}
if (check != sum) { // 如果尝试失败了,按顺序加锁,计算,再解锁
sum = 0;
for (int i = 0; i < segments.length; ++i)
segments[i].lock();
for (int i = 0; i < segments.length; ++i)
sum += segments[i].count;
for (int i = 0; i < segments.length; ++i)
segments[i].unlock();
}
if (sum > Integer.MAX_VALUE)
return Integer.MAX_VALUE;
else
return (int)sum;
}
=============================    
    在JDK1.8中, ConcurrentHashMap的实现发生了一些变化,总体的思想还是不变的。
    引入了一个内部类Node,代理原来的HashEntry。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K, V> next; //不再是final的,而是volatile。所以类似上面remove这种操作,不再需要复制元素,直接改节点指向关系就好。同时volatile尽量保一致性。
}
transient volatile Node<K,V>[] table; //代替原本的segments数组。
同时还有:
  • synchronized代替了原本的lock。
  • 对每个集合维护了一个数量的对象,

你可能感兴趣的:(ConcurrentHashMap实现原理)