一、自我提问

二、整体概括

  • 代码量:JDK1.7中ConcurrentHashMap代码2000行,现在JDK1.8中代码多达6000行,可见其复杂度
  • 数据结构:数组+链表+红黑树
  • 性能:ConcurrentHashMap中数据结构是数组+链表+红黑树基本和HashMap的数据结构一样,log红黑树的时间复杂度是O(logn),数组是O(n)
  • 线程安全:ConcurrentHashMap最大的特点是线程安全,一个线程进行put/remove操作时,对桶(链表 or 红黑树)加上synchronized独占锁;
  • 锁分离:一个线程每次对一个桶(链表 or 红黑树)进行加锁,其他线程仍然可以访问其他桶,老版本的是对多个hash桶抽象出一个segment的模型,一个segment包含多个hash桶,并发的时候针对segment上锁,一个segment一把锁,这样锁的粒度比较粗,并发的效率没有针对单个Hash桶上锁的效率高

三、核心设计

ConcurrentHashMap的核心设计点包括:数据结构、hash函数设计、hash函数冲突解决机制、容量控制、并发处理、性能设计、功能设计等;

1.hash方法设计
  • hash函数首先通过key的自带hashCode()方法获取hash值,自带的hashCode()也就是每个Java对象的父类Object.hashCode()方法,Object.hashCode()方法是根据本地方法实现的,根据查阅资料显示,它其实是通过随机数实现的
  • 根据Object.hashCode()生成的hash值有一个缺点是分布不均衡,hash值的二进制中比特位1很容易集中在高位,于是官网将hash值的二进制中比特位高位像右移动了16位,然后将高位变为0,官方解释这是为了降低高位bit位1集中后对hash值分散性的影响,同时这也是性能,系统损耗,hash分散性的综合考虑
  • 如果你仍然怀疑这样hash冲突比较多,你可以重写key的hashCode()和eques()方法来提高key的hash分散性
  • 其实再简单的理解官方就是通过 XORs 来提高hash值得分散性,因为 XORs 是原生指令效率比较高

下面是hash函数的总结翻译:

/**
     * Spreads (XORs) higher bits of hash to lower and also forces top
     * bit to 0.
     *
     * 通过异或将hash值中的高位转换到低位,将高位的比特位转到低位,强制将高位的比特位转为0
     *
     * Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward.
     * 因为table的大小是2的幂,hash值容易碰撞在高位,特别是当key的集合是一个浮点数的时候,
     * 更加容易产生冲突
     * 因此我们将hash值得高位比特位转移到低位,让比特位的1分散更加均匀,降低高位比特位的影响
     *
     * There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     *
     * 这种做法是一个通过以速度和性能来换取bit分布散性更好的做法,是一个权衡的结果;
     * 因为常见的集合的hash已经分布非常均衡了,这些常见的集合不会受益于这种设计;
     * 也就是大多数情况下,集合的hash分布很均衡,这样的设计对它意义不大,
     * 同时在hash桶中,我们使用树来处理大量的碰撞;
     * 我们仅仅采取了异或bit位这种方式来降低系统性能损耗,这种方式同样也降低了bit位,
     * 高位对分散性的影响,要不是由于表的限制;
     *
     *  (h ^ (h >>> 16)) & HASH_BITS 详细解析:
     *
     *  HashMap的值:h = key.hashCode()) ^ (h >>> 16
     *  比HashMap多并上了一个HASH_BITS;
     *
     *  (h >>> 16):将高比特位转移到低位
     *  (h ^ (h >>> 16)):将高比特位变成 0
     *  (h ^ (h >>> 16)) & HASH_BITS:HASH_BITS是0x7fffffff,该步是为了消除最高位上的负符号
     *  hash的负在ConcurrentHashMap中有特殊意义表示在扩容或者是树节点
     */
    static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }
2.解释一下put方法的流程
  • 通过 int hash = spread(key.hashCode())计算出hash值;
  • 在添加数据之前还需要判断table数据是不是为空,如果是空需要先初始化容量,这算是一个设计点,通过懒加载提高空间利用率,在第一个数据添加到Map的时候才初始化空间;
  • 两层遍历循环,第一层对table数组做遍历,判断新增数据是不是hash桶的第一个元素,如果是直接添加;如果新增的元素不是hash桶的第一个,需要第二次遍历,这次是对hash桶遍历(此时的hash桶有可能是链表数据结构,有可能是红黑树的结构),查找是不是有相同元素存在了,如果存在默认是覆盖,如果不存在就在hash桶中插入新数据;
  • 插入新数据的时候判断,如果插入新数据后hash桶的容量触发 链表转红黑树的阈值 8,则需要将hash桶的链表数据结构转换成红黑树;
  • 插入新数据完成后判断,如果map的容量是不是已经触发扩容的加载因子,如果是,则做扩容操作;
  • 到目前还没介绍到并发的情况:
  • 在初始化map的时候会通过CAS操作判断其它线程是不是正在初始化;
  • 在每次插入数据的时候,如果发现发现当前插入数据的hash桶第一个节点已经存在了,那么将会对hash桶上一把synchronized (f)锁,f 表示hash桶的第一个节点,所以hash桶的数量就是ConcurrentHashMap的最大并发数量;
  • 在插入数据如果正遇到有其它线程正在扩容,当前线程或加入到库容的任务中,帮助正在扩容的线程移动数据,共同加油
3.解释一下get方法

get方法执行流程

首先定位到具体的hash槽,若hash槽不为空,判断第一个结点是否是要查找的结点(判断方法是先比较hash值,若相等则需要地址相等或者equals为true中的一个成立,则是要查找的结点),否则根据hash值是否为负数,将查找操作分派给相应的find函数。若是ForwardingNode,则用find函数转发到nextTable上查找;若是TreeBin结点,调用TreeBin的find函数,根据自身读写锁情况,去红黑树中查找。最后如果是普通结点,则遍历链表来寻找。

从代码上也可以看出,get操作是无锁的 。后文中即使TreeBin的find函数虽然有可能会加TreeBin的内部读锁,但也是非阻塞的。

  • spread计算hash值;
  • 判断table不为空;
  • 定位tabAt(i)所处hash桶位不为空;
  • 检查hash桶第一个元素是不是满,是则返回当前Node的value;否则分别根据树、链表查询;

get方法怎么每次都读取的最新值

  • 从源码中可以看出来,在get操作中,根本没有使用同步机制,也没有使用unsafe方法,所以读操作是支持并发操作的;
  • 那为什么是安全的并发读呢?2点原因
  • 如果get和put是在hash桶的第一个节点冲突,这时候在get和put方法中都使用了CAS操作保证并发安全
  • 如果get和put是在hash桶的非第一个节点冲突,这时候在put方法中使用了synchronized锁住第一个节点,来保证整个hash桶的线程安全
4.“帮助邻居”的思想

解释一下重新扩容的时候,如果多线程并发扩容是怎么实现的,比如“帮助邻居”的思想“。

ConcurrentHashMap无锁多线程扩容,减少扩容时的时间消耗。参考链接3 transfer扩容操作:单线程构建两倍容量的nextTable;允许多线程复制原table元素到nextTable。
为每个内核均分任务,并保证其不小于16;
若nextTab为null,则初始化其为原table的2倍;
死循环遍历,直到finishing。
节点为空,则插入ForwardingNode;
链表节点(fh>=0),分别插入nextTable的i和i+n的位置;
TreeBin节点(fh<0),判断是否需要untreefi,分别插入nextTable的i和i+n的位置;
finishing时,nextTab赋给table,更新sizeCtl为新容量的0.75倍 ,完成扩容

5.怎么计算的容量大小

每个hash桶有一个计数器:CounterCell,有多少个hash桶就有多少个计数器,设计了一个计数器的数;
CounterCell对象里面封装了一个 volatile long value 变量;
每次求整个map的容量的时候,循环计数器数组,累加完成;

6.红黑树和链表互相转

红黑树将链表的查询时间效率从O(n) 提升到了log(n)

7.从map中移除一个元素

删除一个key就是将这个key的value设置为null

删除的逻辑和put有点相似

  • 检查hash桶的第一个节点,是否满足删除要求
  • 如果不满足则对hash桶的第一个节点上锁,然后根据hash桶的数据结构链表或者树,采取不同的查询方法,遍历hash桶,找到满足要求的节点,删除节点。
/**
     * Removes the key (and its corresponding value) from this map.
     * This method does nothing if the key is not in the map.
     *
     * @param  key the key that needs to be removed
     * @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 is null
     */
    public V remove(Object key) {
        return replaceNode(key, null, null);
    }


   /**
     * Implementation for the four public remove/replace methods:
     * Replaces node value with v, conditional upon match of cv if
     * non-null.  If resulting value is null, delete.
     */
    final V replaceNode(Object key, V value, Object cv) {
        int hash = spread(key.hashCode());

        //第一层循环,循环table
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0 ||
                (f = tabAt(tab, i = (n - 1) & hash)) == null){
                // 如果要删除的节点为null,直接退出,hash桶的第一个节点为空
                break;
            } else if ((fh = f.hash) == MOVED){
                // 如果删除数据的时候碰到正在扩容,那么删除线程加入并发扩容的阵营中去,‘帮助思想’的体现
                tab = helpTransfer(tab, f);
            }
            else {
                V oldVal = null;
                boolean validated = false;
                //将hash桶上锁,对第一个节点上锁,锁住入口
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        
                        //遍历链表,寻找满足的节点删除
                        if (fh >= 0) {
                            validated = true;
                            for (Node<K,V> e = f, pred = null;;) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    V ev = e.val;
                                    if (cv == null || cv == ev ||
                                        (ev != null && cv.equals(ev))) {
                                        oldVal = ev;
                                        if (value != null)
                                            e.val = value;
                                        else if (pred != null)
                                            pred.next = e.next;
                                        else
                                            setTabAt(tab, i, e.next);
                                    }
                                    break;
                                }
                                pred = e;
                                if ((e = e.next) == null)
                                    break;
                            }
                        }
                        // 如果发现当前hash桶已经是一棵树了,通过遍历树,删除满足要求的节点
                        else if (f instanceof TreeBin) {
                            validated = true;
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> r, p;
                            if ((r = t.root) != null &&
                                (p = r.findTreeNode(hash, key, null)) != null) {
                                V pv = p.val;
                                if (cv == null || cv == pv ||
                                    (pv != null && cv.equals(pv))) {
                                    oldVal = pv;
                                    if (value != null)
                                        p.val = value;
                                    else if (t.removeTreeNode(p))
                                        setTabAt(tab, i, untreeify(t.first));
                                }
                            }
                        }
                    }
                }
                if (validated) {
                    if (oldVal != null) {
                        if (value == null)
                            addCount(-1L, -1);
                        return oldVal;
                    }
                    break;
                }
            }
        }
        return null;
    }
  1. remove(Object key)
  2. addCount(long x, int check)
  3. helpTransfer(Node<K,V>[] tab, Node<K,V> f)
  4. resizeStamp(int n)
  5. tryPresize(int size)
  6. transfer(Node<K,V>[] tab, Node<K,V>[] nextTab)
  7. fullAddCount(long x, boolean wasUncontended)
  8. treeifyBin()
  9. untreeify(Node<K,V> b)
  10. size()

四、参考连接

  1. 【JUC】JDK1.8源码分析之ConcurrentHashMap(一)
  2. 【JUC】JDK1.8源码分析之ConcurrentSkipListMap(二)
  3. ConcurrentHashMap源码分析–Java8
  4. 为什么ConcurrentHashMap是弱一致的

五、源码翻译

ConcurrentHashMap阅读笔记.java