• ConcurrentHashMap


    《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.  }

  • 相关阅读:
    top、ps -ef、ps aux的区别及内容详解
    img2pdf 报 img2pdf.AlphaChannelError: Refusing to work on images with alpha channel 的解决方案
    Scrapy命令行调用传入自定义参数
    查询Linux CPU架构
    LeetCode 216. 组合总和 III | Python
    LeetCode 40. 组合总和 II | Python
    LeetCode 39. 组合总和 | Python
    LeetCode 77. 组合 | Python
    LeetCode 347. 前 K 个高频元素 | Python
    LeetCode 107. 二叉树的层次遍历 II | Python
  • 原文地址:https://www.cnblogs.com/lijianming180/p/12402018.html
Copyright © 2020-2023  润新知