ConcurrentHashmap的源码分析

ConcurrentHashmap

高效的ConcurrentHashmap采用了锁分离的设计思想。在修改数据时通过加锁实现线程安全(读取时不需要)。

jdk1.7

在JDK1.7中,使用Segment分段锁,将ConcurrentHashMap的Node[]数组分成一段一段,然后给每一段配一把锁(segment),默认总共分配16个Segment锁。

在创建对象时,默认会创建长度为16的Segment[]数组,还会创建长度为2的HashEntry[]数组,并赋值给Segment[]数组的下标为[0]的Segment的table。Segment[0]数组的table,作用是用于扩容的模版。

在插入元素时,如果table为null,就以创建一个长度与Segment[0]数组的table相同的HashEntry[]数组。再次利用键的hash值计算出在HashEntry[]数组应存入的索引。

HashEntry[]数组是会扩容的,每次扩容两倍。Segment的加载因子是给下面的HashEntry[]使用的。

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {
    
    //...
    
    static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;

        HashEntry(int hash, K key, V value, HashEntry<K,V> next) {/*...*/}

        final void setNext(HashEntry<K,V> n) {
            UNSAFE.putOrderedObject(this, nextOffset, n);
        }

        //...
    }
    
    //...


    static final class Segment<K,V> extends ReentrantLock implements Serializable {
    
        //...

        transient volatile HashEntry<K,V>[] table;

        transient int count;

        transient int modCount;

        transient int threshold;

        final float loadFactor;

        Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
            this.loadFactor = lf;
            this.threshold = threshold;
            this.table = tab;
        }

        final V put(K key, int hash, V value, boolean onlyIfAbsent) {/*...*/}

        @SuppressWarnings("unchecked")
        private void rehash(HashEntry<K,V> node) {/*...*/}

        private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {/*...*/}

        private void scanAndLock(Object key, int hash) {/*...*/}

        final V remove(Object key, int hash, Object value) {/*...*/}

        final boolean replace(K key, int hash, V oldValue, V newValue) {/*...*/}

        final V replace(K key, int hash, V value) {/*...*/}
    }
    
    //...
    
}

jdk1.8

底层的Node[]数组使用volatile关键字修饰。

transient volatile Node<K,V>[] table;

会在第一次put元素时初始化。

在JDK1.8中,ConcurrentHashMap取消了segment分段锁(但为了向上兼容仍保留了Segment对象),而采用CAS和synchronized(直接对Node数组的头节点加synchronized锁)来保证并发安全。效率又大大提升!

在put元素时,若此hash桶没有元素,则使用cas保证添加元素的原子性,若有元素则对其第一个元素上synchronized锁,并添加元素。

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {
    
    //...
    
    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
        //Unsafe类的compareAndSwapObject()方法
        //public final native boolean compareAndSwapObject(
        //					Object var1, long var2, Object var4, Object var5);
    }	

    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;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();//table为null或table长度为0时会初始化
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   //若为空桶则无锁添加(此处有cas保证原子性)
                					//casTabAt调用Unsafe类的compareAndSwapObject()方法
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);//发现正在扩容(迁移数据),则会帮助扩容(迁移数据)
            else {
                V oldVal = null;
                synchronized (f) {//对链表头节点(树的根节点)上锁
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                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) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }
    
    //...
}
1.7的Segment分段锁 和 1.8的CAS+synchronized 有何不同?

1.7的Segment分段锁继承了ReentrantLock,尝试获取锁存在并发竞争、自旋、阻塞。HashEntry[]数组的多个链表头共用一把锁。

1.8的cas失败自旋保证成功,再失败就synchronized保证。每个Node的头结点共用一把锁。

ConcurrentHashmap的扩容机制?

参考:https://blog.csdn.net/ZOKEKAI/article/details/90051567

JDK1.8中:

何时触发扩容?

  • 在调用 addCount 方法增加集合元素计数后发现当前集合元素个数到达扩容阈值时就会触发扩容 。

  • 扩容状态下其他线程对集合进行插入、修改、删除、合并、compute等操作时遇到 ForwardingNode 节点会调用该帮助扩容方法helpTransfer()进行扩容。

  • putAll 批量插入或者插入节点后发现存在链表长度达到 8 个或以上,但数组长度为 64 以下时会触发扩容 。

posted @ 2020-07-05 15:39  TCSN  阅读(8)  评论(0编辑  收藏