zoukankan      html  css  js  c++  java
  • JDK1.6和1.8的ConcurrentHashMap实现比较

    1、整体结构
         1.6:Segment (继承ReentrantLock)+ HashEntry 

         1.8: 移除Segment,使锁的粒度更小,Synchronized + CAS + Node (红黑树)+ Unsafe

    2、put()
         1.6:先定位Segment,再定位桶,put全程加锁,没有获取锁的线程提前找桶的位置,并最多自旋64次获取锁,超过则挂起。

         1.8:由于移除了Segment,类似HashMap,可以直接定位到桶,拿到first节点后进行判断,1、为空则CAS插入;2、为-1则说明在扩容,则跟着一起扩容;3、else则加锁put(类似1.7)

    3、get()
         基本类似,由于value声明为volatile,保证了修改的可见性,因此不需要加锁。

    让我们看下1.6源码分析:

    // Segment是一个特殊的hash表,继承ReentrantLock只是只是为了简化一些锁定,避免单独new一个ReentrantLock。
    static final class Segment<K,V> extends ReentrantLock implements Serializable {
        private static final long serialVersionUID = 2249069246763182397L;
        // 类似size,外部修改Segment结构的都会修改这个变量,它是volatile的,几个读方法在一开始就判断这个值是否为0,能尽量保证不做无用的读取操作。
        transient volatile int count;
        // 跟集合类中的modCount一样,检测到这个数值变化说明Segment一定有被修改过
        // 因为modCount不是volatile,有可能无法反映出一次修改操作的中间状态(历史的一致状态/未来的一致状态)
    }
    


    put操作
    // ConcurrentHashMap.put是通过hash值定位到Segment,有这个Segment的put来完成的 // ConcurrentHashMap的实现中不允许null key和null value public V put(K key, V value) { if (value == null) throw new NullPointerException(); int hash = hash(key.hashCode()); // 这里 null key 会抛出NPE return segmentFor(hash).put(key, hash, value, false); } V put(K key, int hash, V value, boolean onlyIfAbsent) { lock(); try { int c = count; // 执行扩容,再添加节点, if (c++ > threshold) // ensure capacity rehash(); HashEntry<K,V>[] tab = table; int index = hash & (tab.length - 1); // 定位方式和1.6的HashMap一样 HashEntry<K,V> first = tab[index]; HashEntry<K,V> e = first; while (e != null && (e.hash != hash || !key.equals(e.key))) e = e.next; V oldValue; if (e != null) { oldValue = e.value; if (!onlyIfAbsent) // put相同的key(相当于replace)不会修改modCount,这降低了containsValue方法的准确性 e.value = value; } else { oldValue = null; ++modCount; // 也是添加在HashEntry链的头部,前面说了,这里的HashEntry的next指针是final的,new后就不能变 tab[index] = new HashEntry<K,V>(key, hash, first, value); count = c; } return oldValue; } finally { unlock(); } } // 扩容时为了不影响正在进行的读线程,最好的方式是全部节点复制一次并重新添加 // 这里根据扩容时节点迁移的性质,最大可能的重用一部分节点,这个性质跟1.8的HashMap中的高低位是一个道理,必须要求hash值是final的 void rehash() { HashEntry<K,V>[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity >= MAXIMUM_CAPACITY) return; // 这里有段注释是说下面的算法及其作用的,跟1.8的HashMap的resize中用到的那个高低位的原理一样: // 扩容前在一个hash桶中的节点,扩容后只会有两个去向。这里是用 &,后续改为用高低位,实质上是一样的。 // 根据这个去向,找到最末尾的去向都一样的连续的一部分,这部分可以重用,不需要复制 // HashEntry的next是final的,resize/rehash时需要重新new,这里的特殊之处就是最大程度重用HashEntry链尾部的一部分,尽量减少重新new的次数 HashEntry<K,V>[] newTable = HashEntry.newArray(oldCapacity<<1); threshold = (int)(newTable.length * loadFactor); // 跟1.6的HashMap有一样的小问题,可能会过早变为Integer.MAX_VALUE从而导致后续永远不能扩容 int sizeMask = newTable.length - 1; for (int i = 0; i < oldCapacity ; i++) { // We need to guarantee that any existing reads of old Map can proceed. So we cannot yet null out each bin. // 为了保证其他线程能够继续执行读操作,不执行手动将原来table赋值为null,只是再最后修改一次table的引用 // 只要其他线程完成了读操作,就不会再引用旧HashEntry,旧的就会自动被垃圾回收器回收 // 关于resize HashEntry<K,V> e = oldTable[i]; if (e != null) { HashEntry<K,V> next = e.next; int idx = e.hash & sizeMask; if (next == null) newTable[idx] = e; else { // Reuse trailing consecutive sequence at same slot HashEntry<K,V> lastRun = e; int lastIdx = idx; // 这个循环是寻找HashEntry链最大的可重用的尾部 // 看过1.8的HashMap就知道,如果hash值是final的,那么每次扩容,扩容前在一条链表上的节点,扩容后只会有两个去向 // 这里重用部分中,所有节点的去向相同,它们可以不用被复制 for (HashEntry<K,V> last = next; last != null; last = last.next) { int k = last.hash & sizeMask; if (k != lastIdx) { lastIdx = k; lastRun = last; } } newTable[lastIdx] = lastRun; // 把重用部分整体放在扩容后的hash桶中 // 复制不能重用的部分,并把它们插入到rehash后的所在HashEntry链的头部 for (HashEntry<K,V> p = e; p != lastRun; p = p.next) { int k = p.hash & sizeMask; HashEntry<K,V> n = newTable[k]; newTable[k] = new HashEntry<K,V>(p.key, p.hash, n, p.value); } // 这里也可以看出,重用部分rehash后相对顺序不变,并且还是在rehash后的链表的尾部 } } } table = newTable; }
    get操作
    // ConcurrentHashMap.get是通过hash定位到Segment,再让这个Segment的get来完成的 public V get(Object key) { int hash = hash(key.hashCode()); return segmentFor(hash).get(key, hash); // 直接读value是不用加锁的,碰到读到value == null,才加锁再读一次,这个前面说了,后面的也一样 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); // recheck } e = e.next; } } return null; }

    对比1.8源码分

    put操作

    public V put(K key, V value) { return putVal(key, value, false); } /** Implementation for put and putIfAbsent */ final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; //这里就是上面构造方法没有进行初始化,在这里进行判断,为null就调用initTable进行初始化,属于懒汉模式初始化 if (tab == null || (n = tab.length) == 0) tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //如果i位置没有数据,就直接cas插入 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } else if ((fh = f.hash) == MOVED) //如果在进行扩容,则先进行扩容操作 tab = helpTransfer(tab, f); else { //如果以上条件都不满足,那就要进行加锁操作,也就是存在hash冲突,锁住链表或者红黑树的头节点 V oldVal = null; synchronized (f) { if (tabAt(tab, i) == f) { if (fh >= 0) { //表示该节点是链表结构 binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; //这里涉及到相同的key进行put就会覆盖原先的value if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { //插入链表尾部 pred.next = new Node<K,V>(hash, key, value, null); break; } } } else if (f instanceof TreeBin) { //红黑树结构 Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } if (binCount != 0) { //如果链表的长度大于8时就会进行红黑树的转换 if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } //统计size,并且检查是否需要扩容 addCount(1L, binCount); return null;
    }

    get操作
    public V get(Object key) { Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; //计算hash int h = spread(key.hashCode()); //读取首节点的Node元素 if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) { if ((eh = e.hash) == h) { //如果该节点就是首节点就返回 if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; }
    else if (eh < 0) return (p = e.find(h, key)) != null ? p.val : null; while ((e = e.next) != null) { if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) return e.val; } } return null; }

            可以看出,相对于JDK1.8之前的Segment分段锁写操作,在操作之前需要先获取lock锁,而在JDK1.8的写操作中,如果该链表不存在,添加链表头的时候是不需要加锁的,因为是往哈希表table数组的某个位置填充值,不需要遍历链表之类的,所以可以基于unfase的casTabAt方法,即CAS机制检查table数组的该位置是否存在元素(链表头结点)来实现线程安全,这是写操作最先检查的;如果该链表已经存在,则需要通过synchronized来锁住该链表头结点,而在JDK1.7的实现中是锁住该Segment内部的整个哈希表table数组,所以这里也是一个性能提升的地方,缩小了锁的粒度。
           另外就是1.8查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。

    还有另外的很多方法,研究的不是很透彻,等再研究研究然后发出来个人见解

  • 相关阅读:
    第十六周学习进度报告
    个人课程总结
    第一阶段意见评论
    用户评价
    第二阶段10
    第二阶段9
    第二阶段8
    第十五周学习进度报告
    第二阶段7
    第二阶段6
  • 原文地址:https://www.cnblogs.com/innocenter/p/12895767.html
Copyright © 2011-2022 走看看