ConcurrentHashMap 1.8 源码解读 (面试杀手锏)

1.本文简介

1.1 ConcurrentHashMap put方法介绍

2.源码解读

2.1 put方法解读

调用put(K key, V value)方法实际调用的是putVal(key, value, false),如下:

public V put(K key, V value) {
    return putVal(key, value, false);
}

所以,解读重点在putVal方法上。先看一下putVal方法操作主要流程:

1.若 key == null || value == null,抛出 NPE,结束;否则,计算key的hash值,继续往下执行

2.对存放的 Node[] tab 数组进行for循环遍历,for (Node[] tab = table;;),for循环退出交由执行体控制;

执行体进行如下一系列判断:

    2.1 判断tab是否初始化,未初始化则进行初始化(首次初始化默认数组长度为16);

 if (tab == null || (n = tab.length) == 0)
    tab = initTable();

    2.2 判断key对应的数组位置上是否为null,若尚未发生hash碰撞,即进行CAS操作,new 一个 Node存放到tab中,退出for循环;

else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    if (casTabAt(tab, i, null,
                 new Node(hash, key, value, null)))
        break;                   // no lock when adding to empty bin
}

    2.3 判断tab是否需要进行扩容:

 else if ((fh = f.hash) == MOVED)  // MOVED = -1
    tab = helpTransfer(tab, f);

    2.4 以上判断都未进入,说明tab已初始化,且有容量存放数据,但发生了hash冲突,接下来则进入hash冲突解决阶段:

         先用synchronized 锁住f,即tab数组发生hash冲突位置上的元素对象Node,然后再判断一下tabAt(tab, i) == f,主要作用是tab是一个volatile修饰的对所有线程可见的共享变量,在synchronized 上锁成功之前其他线程有可能对tab做了操作,如扩容,remove等。

        2.4.1 if (fh >= 0) 当做链表处理:进行for循环,若hash 和 key 都相同,则用新值替换旧值,break;否则一直循环到链表尾,当 (e = e.next) == null 时,pred.next = new Node,添加数据到链表,break; 注意此处 ++binCount,binCount用来统计链表长度;

        2.4.2 else if (f instanceof TreeBin) 当做树结构处理:若添加的key不存在于树中,则 putTreeVal 方法直接添加并返回null,若是key对应的数据已存在,putTreeVal 方法返回树中的Node p,并用新值替换旧值;

        2.4.3 在synchronized 代码块执行结束后,判断链表长度是否 >=  阈值 8(TREEIFY_THRESHOLD),是则转为红黑树;

之后要么 return oldVal ,要么break 结束for循环;

else {
    V oldVal = null;
    synchronized (f) {
        if (tabAt(tab, i) == f) {
            if (fh >= 0) {
                binCount = 1;
                for (Node e = f;; ++binCount) {
                    K ek;
                    if (e.hash == hash &&
                        ((ek = e.key) == key ||
                         (ek != null && key.equals(ek)))) {    // key 存在,更新
                        oldVal = e.val;
                        if (!onlyIfAbsent)
                            e.val = value;
                        break;
                    }
                    Node pred = e;
                    if ((e = e.next) == null) {
                        pred.next = new Node(hash, key,
                                                  value, null);    //key 不存在,链表中追加新元素
                        break;
                    }
                }
            }
            else if (f instanceof TreeBin) {
                Node p;
                binCount = 2;
                //key不存在则putTreeVal方法直接添加新元素并返回null,key存在则返回对应节点p并做val更新
                if ((p = ((TreeBin)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;
    }
}

3. for 循环处理结束后,检查tab是否需要扩容:

  addCount(1L, binCount);   //检查扩容

4. putVal 方法执行结束: return null;

2.2 put 方法流程图

    为更直观了解put方法的执行过程,画个流程图:

ConcurrentHashMap 1.8 源码解读 (面试杀手锏)_第1张图片 ConcurrentHashMap  put 流程图

 2.3 细节探讨

    2.3.1 链表转红黑树

    1. 链表转红黑树阈值 TREEIFY_THRESHOLD 8 的问题,看源码,是用binCount来比较;但binCount并不能完全正确的及时反映链表的长度。看下面这段代码:

synchronized (f) {
    if (tabAt(tab, i) == f) {
        if (fh >= 0) {
            binCount = 1;
            for (Node 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 pred = e;
                if ((e = e.next) == null) {
                    pred.next = new Node(hash, key,
                                              value, null);
                    break;
                }
            }
        }
        else if (f instanceof TreeBin) {
            Node p;
            binCount = 2;
            if ((p = ((TreeBin)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;
}

(1)假设本次key存在,且在链表的中间位置,那么break的时候 binCount 不能代表链表的实际长度,此时实际长度可能已达             到8;

(2)假设本次key不存在,且链表长度已经为7,那么添加新元素后,实际链表长度已经是8,但break时,binCount 还是7;

(3)至于阈值为什么为8,哈哈,不清楚哈,估计是基于测试统计给出来的吧。有个 泊松分布 概率函数,感兴趣的可以了解              下。

  2. 链表转红黑树,关于tab 数组长度的问题。看代码:

/**
 * Replaces all linked nodes in bin at given index unless table is
 * too small, in which case resizes instead.
 */
private final void treeifyBin(Node[] tab, int index) {
    Node b; int n, sc;
    if (tab != null) {
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            tryPresize(n << 1);    //MIN_TREEIFY_CAPACITY 默认64,tryPresize 扩容
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            synchronized (b) {
                if (tabAt(tab, index) == b) {
                    //红黑树转化
                    TreeNode hd = null, tl = null;
                    for (Node e = b; e != null; e = e.next) {
                        TreeNode p =
                            new TreeNode(e.hash, e.key, e.val,
                                              null, null);
                        if ((p.prev = tl) == null)
                            hd = p;
                        else
                            tl.next = p;
                        tl = p;
                    }
                    setTabAt(tab, index, new TreeBin(hd));
                }
            }
        }
    }
}

 (1)MIN_TREEIFY_CAPACITY 为64,可见,当链表长度达到8但tab长度小于64时,并不会直接进行红黑树转化,而是对tab进行扩容,根据新的new_tab大小以及key的hash值,重新分配index,将原有元素copy到新的new_tab中,且原有元素若为链表,扩容后理论上分为2个小链表,链表位置为 index=i 和 index=i+n ,n 表示原有tab大小;若原有元素为树结构,扩容后理论上分为2个小树,树位置为 index=i 和 index=i+n ,n 表示原有tab大小,若小树中元素小于等于UNTREEIFY_THRESHOLD 6 ,扩容时会向下转化为链表,否则继续生成树结构;由此也可见,当链表长度达到8但tab长度小于64时,采用的是扩容来降低hash冲突,从而理论上降低链表长度。

3.小结

3.1 ConcurrentHashMap 1.8 put 方法主要采用 CAS (hash未冲突使用) + synchronized (hash冲突使用),来解决并发时的线程安全问题;

3.2 链表转为红黑树,必要条件是  数组长度不小于64,且链表长度不小于8;

4.附完整源码

/**
 * Maps the specified key to the specified value in this table.
 * Neither the key nor the value can be null.
 *
 * 

The value can be retrieved by calling the {@code get} method * with a key that is equal to the original key. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with {@code key}, or * {@code null} if there was no mapping for {@code key} * @throws NullPointerException if the specified key or value is null */ 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[] tab = table;;) { Node f; int n, i, fh; if (tab == null || (n = tab.length) == 0) tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node(hash, key, value, null))) break; // no lock when adding to empty bin } 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 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 pred = e; if ((e = e.next) == null) { pred.next = new Node(hash, key, value, null); break; } } } else if (f instanceof TreeBin) { Node p; binCount = 2; if ((p = ((TreeBin)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; }

评论区欢迎讨论!觉得有用点个赞再走吧。感谢!!!

你可能感兴趣的:(Java,源码解读)