码迷,mamicode.com
首页 > 其他好文 > 详细

ConcurrentHashMap

时间:2020-03-03 14:54:42      阅读:58      评论:0      收藏:0      [点我收藏+]

标签:elements   hash冲突   方法   sam   另一个   复杂   存在   tab   when   

《Concurrent包中的锁机制》http://www.iteye.com/topic/333669
《java.util.concurrent 之ConcurrentHashMap 源码分析》http://www.iteye.com/topic/977348
《ConcurrentHashMap之实现细节》http://www.iteye.com/topic/344876

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

重新hash的算法

1
privatestatic int hash(int h) {  
   // 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);  
}

定位段的方法
finalSegment segmentFor(int hash) { return segments[(hash >>> segmentShift) & segmentMask];

数据结构
Hash表,解决hash冲突,ConcurrentHashMap和HashMap使用相同的方式,都是将hash值相同的节点放在一个hash链中。与HashMap不同的是,ConcurrentHashMap使用多个子Hash表,也就是段(Segment)。下面是ConcurrentHashMap的数据成员:

1
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>  implements ConcurrentMap<K, V>, Serializable { 
 
   final int segmentMask;  
 
   final int segmentShift;  
 
   final Segment<K,V>[] segments;  
}

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

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

1
   static final class Segment<K,V> extends ReentrantLock implements Serializable {  
private static final long serialVersionUID = 2249069246763182397L;  
       /**
        * The number of elements in this segment's region.
        */  
       transient volatile int count;  
//如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。
//java的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化。
 
       /**
        * Number of updates that alter the size of thetable. This is
        * used during bulk-read methods to make sure theysee a
        * consistent snapshot: If modCounts change during atraversal
        * of segments computing size or checkingcontainsValue, then
        * we might have an inconsistent view of state so(usually)
        * must retry.
        */  
       transient int modCount;  
 
       /**
        * The table is rehashed when its size exceeds thisthreshold.
        * (The value of this field is always<tt>(int)(capacity *
        * loadFactor)</tt>.)
        */  
       transient int threshold;  
 
       /**
        * The per-segment table.
        */  
       transient volatile HashEntry<K,V>[] table;  
 
       /**
        * The load factor for the hash table.  Eventhough this value
        * is same for all segments, it is replicated toavoid 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表示负载因子。

实现细节-修改操作


删除操作remove(key)

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

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

下面是Segment的remove方法实现:

1
1.    V remove(Object key, int hash, Object value) {  
2.        lock();  //持有段锁
3.        try { //定位到要删除的节点e
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.  //【关键原理】将e前面的结点复制一遍,尾结点指向e的下一个结点。e后面的结点不需要复制,它们可以重用
13.          V oldValue = null;  
14.          if (e != null) {  
15.              V v = e.value;  
16.              if (value == null || value.equals(v)) {  
17.                  oldValue = v;  
18.                  // All entries following removed node can stay  
19.                  // in list, but all preceding ones need to be  
20.                  // cloned.  
21.                  ++modCount;  
22.                  HashEntry<K,V> newFirst = e.next;  
23.                  for (HashEntry<K,V> p = first; p != 大专栏  ConcurrentHashMap e; p = p.next)  
24.                      newFirst = new HashEntry<K,V>(p.key, p.hash,  
25.                                                    newFirst, p.value);  
26.                  tab[index] = newFirst;  
27.                  count = c; // write-volatile  
28.              }  
29.          }  
30.          return oldValue;  
31.      } finally {  
32.          unlock();  
33.      }  
34.  }

整个操作是在持有段锁的情况下执行的,空白行之前的行主要是定位到要删除的节点e。
接下来,如果不存在这个节点就直接返回null,否则就要将e前面的结点复制一遍,尾结点指向e的下一个结点。e后面的结点不需要复制,它们可以重用。

整个remove实现并不复杂,但是需要注意如下几点。第一,当要删除的结点存在时,删除的最后一步操作要将count的值减一。这必须是最后一步操作,否则读取操作可能看不到之前对段所做的结构性修改。
第二,remove执行的开始就将table赋给一个局部变量tab,这是因为table是volatile变量,读写volatile变量的开销很大。编译器也不能对volatile变量的读写做任何优化,直接多次访问非volatile实例变量没有多大影响,编译器会做相应优化。

put操作


put操作也是委托给段的put方法。下面是段的put方法:

1
1.    V put(K key, int hash, V value, boolean onlyIfAbsent) {  
2.        lock();  
3.        try {  
4.            int c = count;  
5.            if (c++ > threshold) // ensure capacity  
6.                rehash();  
7.            HashEntry<K,V>[] tab = table;  
8.            int index = hash & (tab.length - 1);  
9.            HashEntry<K,V> first = tab[index];  
10.          HashEntry<K,V> e = first;  
11.          while (e != null && (e.hash != hash || !key.equals(e.key)))  
12.              e = e.next;  
13.    
14.          V oldValue;  
15.          if (e != null) {  
16.              oldValue = e.value;  
17.              if (!onlyIfAbsent)  
18.                  e.value = value;  
19.          }  
20.          else {  
21.              oldValue = null;  
22.              ++modCount;  
23.              tab[index] = new HashEntry<K,V>(key, hash, first, value);  
24.              count = c; // write-volatile  
25.          }  
26.          return oldValue;  
27.      } finally {  
28.          unlock();  
29.      }  
30.  }

该方法也是在持有段锁的情况下执行的,首先判断是否需要rehash,需要就先rehash。接着是找是否存在同样一个key的结点,如果存在就直接替换这个结点的值。否则创建一个新的结点并添加到hash链的头部,这时一定要修改modCount和count的值,同样修改count的值一定要放在最后一步。put方法调用了rehash方法,reash方法实现得也很精巧,主要利用了table的大小为2^n,这里就不介绍了。

修改操作还有putAll和replace。putAll就是多次调用put方法,没什么好说的。replace甚至不用做结构上的更改,实现要比put和delete要简单得多,理解了put和delete,理解replace就不在话下了,这里也不介绍了。

获取操作


get操作,同样ConcurrentHashMap的get操作是直接委托给Segment的get方法,直接看Segment的get方法:

1
1.    V get(Object key, int hash) {  
2.        if (count != 0) { // read-volatile  
3.            HashEntry<K,V> e = getFirst(hash);  
4.            while (e != null) {  
5.                if (e.hash == hash && key.equals(e.key)) {  
6.                    V v = e.value;  
7.                    if (v != null)  
8.                        return v;  
9.                    return readValueUnderLock(e); // recheck  
10.              }  
11.              e = e.next;  
12.          }  
13.      }  
14.      return null;  
15.  }

get操作不需要锁。第一步是访问count变量,这是一个volatile变量,由于所有的修改操作在进行结构修改时都会在最后一步写count变量,通过这种机制保证get操作能够得到几乎最新的结构更新。
对于非结构更新,也就是结点值的改变,由于HashEntry的value变量是volatile的,也能保证读取到最新的值。
接下来就是对hash链进行遍历找到要获取的结点,如果没有找到,直接访回null。
对hash链进行遍历不需要加锁的原因在于链指针next是final的。但是头指针却不是final的,这是通过getFirst(hash)方法返回,也就是存在table数组中的值。这使得getFirst(hash)可能返回过时的头结点,例如,当执行get方法时,刚执行完getFirst(hash)之后,另一个线程执行了删除操作并更新头结点,这就导致get方法中返回的头结点不是最新的。这是可以允许,通过对count变量的协调机制,get能读取到几乎最新的数据,虽然可能不是最新的。要得到最新的数据,只有采用完全的同步。

最后,如果找到了所求的结点,判断它的值如果非空就直接返回,否则在有锁的状态下再读一次。这似乎有些费解,理论上结点的值不可能为空,这是因为put的时候就进行了判断,如果为空就要抛NullPointerException。空值的唯一源头就是HashEntry中的默认值,因为HashEntry中的value不是final的,非同步读取有可能读取到空值。仔细看下put操作的语句:tab[index] = new HashEntry(key, hash, first, value),在这条语句中,HashEntry构造函数中对value的赋值以及对tab[index]的赋值可能被重新排序,这就可能导致结点的值为空。这种情况应当很罕见,一旦发生这种情况,ConcurrentHashMap采取的方式是在持有锁的情况下再读一遍,这能够保证读到最新的值,并且一定不会为空值。

1
1.    V readValueUnderLock(HashEntry<K,V> e) {  
2.        lock();  
3.        try {  
4.            return e.value;  
5.        } finally {  
6.            unlock();  
7.        }  
8.    }

containsKey

1
1.    boolean containsKey(Object key, int hash) {  
2.        if (count != 0) { // read-volatile  
3.            HashEntry<K,V> e = getFirst(hash);  
4.            while (e != null) {  
5.                if (e.hash == hash && key.equals(e.key))  
6.                    return true;  
7.                e = e.next;  
8.            }  
9.        }  
10.      return false;  
11.  }

ConcurrentHashMap

标签:elements   hash冲突   方法   sam   另一个   复杂   存在   tab   when   

原文地址:https://www.cnblogs.com/lijianming180/p/12402018.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!