标签: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
数据结构
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
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. } |
标签:elements hash冲突 方法 sam 另一个 复杂 存在 tab when
原文地址:https://www.cnblogs.com/lijianming180/p/12402018.html