jdk1.8 ConcurrentHashMap 源码学习

 

上次讲到HashMap,但是HashMap并不是线程安全的,那么有哪些线程安全的Map或者是实现线程安全的map呢?

1、 HashTable(已弃用),使用的是内置对象锁对map进行同步,并发执行的效率比较低(key和value均不能为null,因为这是用在多线程的,当get返回null时,无法确定是不包含这个key还是值为null,hashMap允许key为null,因为运行在单线程,可以通过containKey来判断是否存在key,而containKey在多线程中可能刚好在你调用之前remove了当前key导致当前key为null);

2、 ConcurrentHashMap(流行使用),JDK1.7使用分段锁,JDK1.8使用的是CAS+synchronized实现并发访问(key和value均不能为null);

3、使用Collections的synchronizedMap(Map m) 进行包装,使用的是传入m的内置锁,同样并发执行效率低。

下面记录一下JDK1.8的ConcurrentHashMap的源码学习,花了挺长时间,比hashmap难多了

属性

  下面只列出了一些属性

// 保存键值对总数
private transient volatile long baseCount;

/* 默认为0,用来控制table的初始化和扩容操作, 小于0时代表正在扩容,并且 -n表示
* 有n – 1个线程在扩容,正数代表table容量
*/
private transient volatile int sizeCtl;
static final int MOVED     = -1; // hash for forwarding nodes,forwarding nodes 是扩容时用到的node
static final int TREEBIN   = -2; // hash for roots of trees
static final int RESERVED  = -3; // hash for transient 

构造方法

// 带容量的初始化,只是初始化了容量,并没有建立桶数组
public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
}

这里只列出了带容量的构造方法,还有其他的没列出了。

put()方法

public V put(K key, V value) {
        return putVal(key, value, false);
}
// onlyIfAbsent 为 true的话表示若当前key-value不存在,进行插入;若存在,不对当前存在key-value进行更新
final V putVal(K key, V value, boolean onlyIfAbsent) {
        // key 和value不能为null
        if (key == null || value == null) throw new NullPointerException();
            //计算key的哈希值
        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)
                    // 初始化表数组(或者叫桶数组,默认容量为16),和hashMap一样,table是延迟加载的,initTable()通过CAS机制实现同步,稍后会讲(看到这可以先到后面看看initTable())
                tab = initTable();
                    
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {                    
              // 通过CAS机制进行并发put,
              /*
               *param1 :桶数组
               *param2 :节点(node)位置偏移
               *param3 :节点当前预期内存值
               *param4 :要在当前节点内更新的值
              */
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
                // 常量 MOVED = -1,表示正在扩容
            else if ((fh = f.hash) == MOVED)
       //内置锁是加在每个桶上的,扩容实际上是对每个桶上的元素重新分配桶,扩容可以在不同的桶上多线程并发执行
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
        // 对桶 f 进行同步put操作
                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;
                                }
                            }
                        }
                            // f < 0 
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                       // 赋值为2的意义只是为了不等于0?不太清楚,addCount(1L, binCount)看到如果在桶数组不为空时,binCount <= 1会直接返回,是这个原因?
                            binCount = 2;
               // 红黑树节点,加入红黑树中
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                        // TREEIFY_THRESHOLD=8,树化阈值
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 数量+1,并进行是否扩容判断
        addCount(1L, binCount);
        return null;
    }
View Code

initTable() 方法

private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0) // 有线程在初始化,当前线程让步
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
              //若CAS成功 把当前对象SIZECTL偏移位置修改为-1,即sizeConrol = -1
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
            // 用减法来替代 *0.75,计算机中乘法的操作比减法耗时
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }
View Code

addCount()方法

private final void addCount(long x, int check) {
     // counterCells 在 currentMap 初始化和 put 过程都没有进行初始化,本人暂时也不知道这是用来干嘛的,doc注释为Table of counter cells. When non-null, size is a power of 2.
        CounterCell[] as; long b, s;
        // counterCells为null,不进入if语句
    if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();
        }
    // 尝试扩容
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
            /* resizeStamp(n){ // RESIZE_STAMP_BITS = 16
             *return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
             *}
             */
                int rs = resizeStamp(n);
                if (sc < 0) {
                // RESIZE_STAMP_SHIFT = 16 ,下面的if语句判断条件是jdk的bug,在网上查资料一大佬提到 oracle bug库链接:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8214427
                //下列的解释参考自:https://www.jianshu.com/p/749d1b8db066
                // 如果 sc 的低 16 位不等于 标识符(校验异常 sizeCtl 变化了)
                // 如果 sc == 标识符 + 1 (扩容结束了,不再有线程进行扩容)(默认第一个线程设置 sc ==rs 左移 16 位 + 2,当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1)
                // 如果 sc == 标识符 + 65535(帮助线程数已经达到最大)
                // 如果 nextTable == null(结束扩容了)
                // 如果 transferIndex <= 0 (转移状态变化了)
                // 结束循环
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt); // 线程加入协同扩容
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null); //扩容
                s = sumCount();
            }
        }
    }
View Code

transfer()方法

  暂时没有认真研读,了解大概过程,这里先给参考链接,下次再仔细研读一下:

  https://www.jianshu.com/p/aaf769fdbd20

总结

  相比 hashMap ,concurrentHashMap复杂很多,在处理并发安全的问题上,ConcurrenthahMap用到了 CAS + syschroynized,CAS这就要求了要了解java的内存模型,计算机的底层,所以在这些上面花了一部分时间,对 synchronized现在我也还是一知半解,要去啃啃源码,下次博客就要记录synchronized 和 lock、红黑树、线程池原理等等许多java知识,任重而道远。。。

 

参考链接

https://www.jianshu.com/p/c0642afe03e0

https://www.jianshu.com/p/aaf769fdbd20

posted @ 2019-05-11 01:07  X_huang  阅读(177)  评论(0编辑  收藏  举报