HashMap和ConcurrentHashMap源码解读

933 阅读34分钟

一、HashMap源码解读

1、put操作

  • 对key的hashCode进行hash,然后再计算index;

  • 如果没有碰撞直接放到桶bucket里

  • 如果碰撞里了,以链表的形式存在bucket里

  • 如果碰撞导致链表长度过长(大于等于TREEIFY_THRESHOLD),就把链表转换为红黑树

  • 如果节点已经存在就替换old value(保证key的唯一性)

  • 如果bucket满了(超过load factor*current capacity),就要resize.


public V put(K key, V value) {
    //对key的hashCode进行hash
    return putVal(hash(key), key, value, false, true);
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    //hash table对象
    Node<K,V>[] tab;
    //下标i对应的Node对象
    Node<K,V> p;
    //key在hash table中存放的索引下标
    int n, i;
    // 获取hash table 的长度
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // i = (n - 1) & hash 根据hash值计算key在hash table中的位置,根据这行代码可以得出一个结论:如果key为null(此时对应的hash为0),那么一定是在下标为0的位置
    if ((p = tab[i = (n - 1) & hash]) == null)
    //如果下标i的位置是null(尚未有元素),那么直接放入
        tab[i] = newNode(hash, key, value, null);
     // 桶中已经存在元素
    else {
        Node<K,V> e; K k;
        // 比较桶中第一个元素(数组中的结点)的hash值是否相等(p的key和输入的key相等)
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
        // 将第一个元素赋值给e,用e来记录
            e = p;
        // hash值不相等,即key不相等;为红黑树结点
        else if (p instanceof TreeNode)
        // 此时已经是个红黑树了,继续往红黑树里添加元素
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //该链为链表
        else {
           // 如果是传统的列表对象(在链表最末插入结点)
            for (int binCount = 0; ; ++binCount) {
             // 到达链表的尾部(此if的目的是找到位置i上的链表/红黑树的最后一个Node元素)
                if ((e = p.next) == null) {
                 // 在尾部插入新结点
                    p.next = newNode(hash, key, value, null);
                  // 结点数量达到阈值(TREEIFY_THRESHOLD = 8),转化为红黑树(如果hash table的长度不到MIN_TREEIFY_CAPACITY即64,那么只做扩容处理,并不会转换为红黑树)
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                // 跳出循环
                    break;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        // 表示在桶中找到key值、hash值与插入元素相等的结点
        if (e != null) { // existing mapping for key
       // 记录e的value
            V oldValue = e.value;
        // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
              //用新值替换旧值
                e.value = value;
             // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
 // 结构性修改
    ++modCount;
  // 实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
 // 插入后回调
    afterNodeInsertion(evict);
    return null;
}

2、resize的实现

当put时,如果发现目前的bucket占用程度已经超过了Load Factor所希望的比例,那么就会发生resize。在resize的过程,简单的说就是把bucket扩充为2倍,之后重新计算index,把节点再放到新的bucket中。当超过限制的时候会resize,然而又因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。

final Node<K,V>[] resize() {
   //旧数组
    Node<K,V>[] oldTab = table;
    //旧容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //旧扩容门槛
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
       // 超过最大值就不再扩充了,就只好随你碰撞去吧
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 如果旧容量的两倍小于最大容量并且旧容量大于默认初始值(16),则容量扩大为两倍,扩容门槛也扩大为两倍,没超过最大值,就扩充为原来的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 使用非默认构造方法创建的map,第一次插入元素会走到这里
     // 如果旧容量为0且旧扩容门槛大于0,则把新容量赋值为旧门槛
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
        //调用默认构造方法创建的map,第一次插入元素会走到这里
        //如果旧容量、旧扩容门槛都是0,说明还未初始化过,则初始化容量为默认容量,扩容门槛为默认容量*默认装载因子
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
   // 计算新的resize上限
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //赋值扩容门槛为新门槛
    threshold = newThr;
    //新建一个新容量的数组
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //把桶赋值为新数组
    table = newTab;
    //如果旧数组不为空,则搬移元素
    if (oldTab != null) {
    //遍历数组, 把每个bucket都移动到新的buckets中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //如果桶中的第一个元素不为空,赋值给e
            if ((e = oldTab[j]) != null) {
            //旧桶赋值为null,便于GC回收
                oldTab[j] = null;
                //如果这个桶中只有一个元素,则计算他在新桶中的位置并把它搬移到新桶中
                //因为每次都扩容为两倍,所以这里的第一个元素搬移到新桶的时候肯定还没有元素
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                    //如果第一个元素是树节点,则把这棵树打散成两颗树插入到新桶中
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //如果这个链表不止一个元素且不是一颗树,则分化成两个链表插入到新的桶中,比如,假如原来容量为4,3、7、11、15这四个元素都在三号桶中,现在扩容到8,则3和11还在三号桶,7和15要搬移到七号桶中,也就是分化成了两个链表
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                         // (e.hash & oldCap) == 0的元素放在低位链表中
                        // 比如,3 & 4 == 0
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                          // (e.hash & oldCap) != 0的元素放在高位链表中
                            // 比如,7 & 4 != 0
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                       // 遍历完成分化成两个链表了
                    // 低位链表在新桶中的位置与旧桶一样(即3和11还在三号桶中)
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 原索引+oldCap放到bucket里(高位链表在新桶中的位置正好是原来的位置加上旧容量(即7和15搬移到七号桶了))
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

resize过程中对Hash表数组大小的修改使用的是2次幂的扩展(指长度为原来的2倍),这样有两个好处

  • 1、在hashmap的源码中。put方法会调用indexFor(int h, int length)方法,这个方法主要是根据key的hash值找到这个entry在Hash表数组中的位置,源码如下:
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}

上述代码也相当于对length求模。 注意最后return的是h&(length-1)。如果length不为2的幂,比如15。那么length-1的2进制就会变成1110。在h为随机数的情况下,和1110做&操作。尾数永远为0。那么0001、1001、1101等尾数为1的位置就永远不可能被entry占用。这样会造成浪费,不随机等问题。 length-1 二进制中为1的位数越多,那么分布就平均。

  • 2、以下图为例,其中图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,n代表length。
    元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:

resize过程中不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图(一方面位运算更快,另一方面抗碰撞的Hash函数其实挺耗时的):

3、putTreeVal()方法

//插入元素到红黑树的方法
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                       int h, K k, V v) {
            Class<?> kc = null;
            //标记是否找到这个key的节点
            boolean searched = false;
            //找到树的根节点
            TreeNode<K,V> root = (parent != null) ? root() : this;
            //从树的根节点开始遍历
            for (TreeNode<K,V> p = root;;) {
                // dir=direction,标记是在左边还是右边
                // ph=p.hash,当前节点的hash值
                int dir, ph;
                //pk=p.key,当前节点的key值
                K pk;
                if ((ph = p.hash) > h)
                //当前hash比目标hash大,说明在左边
                    dir = -1;
                else if (ph < h)
                //当前hash比目标hash小,说明在右边
                    dir = 1;
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                //两者hash相同且key相等,说明找到了节点,直接返回节点
                //回到putVal()中判断是否需要修改其value值
                    return p;
                else if ((kc == null &&
                  // 如果k是Comparable的子类则返回其真实的类,否则返回null
                          (kc = comparableClassFor(k)) == null) ||
                           // 如果k和pk不是同样的类型则返回0,否则返回两者比较的结果
                         (dir = compareComparables(kc, k, pk)) == 0) {
                           // 这个条件表示两者hash相同但是其中一个不是Comparable类型或者两者类型不同
            // 比如key是Object类型,这时可以传String也可以传Integer,两者hash值可能相同
            // 在红黑树中把同样hash值的元素存储在同一颗子树,这里相当于找到了这颗子树的顶点
            // 从这个顶点分别遍历其左右子树去寻找有没有跟待插入的key相同的元素
                    if (!searched) {
                        TreeNode<K,V> q, ch;
                        searched = true;
                        //遍历左右子树找到了直接返回
                        if (((ch = p.left) != null &&
                             (q = ch.find(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                             (q = ch.find(h, k, kc)) != null))
                            return q;
                    }
                    //如果两者类型相同,再根据它们的内存地址计算hash值进行比较
                    dir = tieBreakOrder(k, pk);
                }

                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                //如果最后确实没有找到对应key的元素,则新建一个节点
                    Node<K,V> xpn = xp.next;
                    TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    xp.next = x;
                    x.parent = x.prev = xp;
                    if (xpn != null)
                        ((TreeNode<K,V>)xpn).prev = x;
                      // 插入树节点后平衡
                     // 把root节点移动到链表的第一个节点
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
            }
        }

4、treeifyBin方法(插入元素后链表的长度大于8则判断是否需要树化)

/**
     *如果hash table的长度大于64,则将指定位置上的所有节点转换为TreeNode;
     *否则只对hast table进行扩容
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
              // 如果桶数量小于64,直接扩容而不用树化
             // 因为扩容之后,链表会分化成两个链表,达到减少元素的作用
            // 当然也不一定,比如容量为4,里面存的全是除以4余数等于3的元素
           // 这样即使扩容也无法减少链表的长度
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
        //转换为红黑树
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
             // 如果进入过上面的循环,则从头节点开始树化
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }
    

5、treeify()方法

   //真正的树化
    final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                 // 第一个元素作为根节点且为黑节点,其它元素依次插入到树中再做平衡
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    // 从根节点查找元素插入的位置
                    for (TreeNode<K,V> p = root;;) {
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h)
                            dir = 1;
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);
                         // 如果最后没找到元素,则插入
                        TreeNode<K,V> xp = p;
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            //插入后平衡,默认插入的是红节点,在balanceInsertion()方法里
                            root = balanceInsertion(root, x);
                            break;
                        }
                    }
                }
            }
            // 把根节点移动到链表的头节点,因为经过平衡之后原来的第一个元素不一定是根节点了
            moveRootToFront(tab, root);
        }

6、get操作

  • a.bucket里的第一个节点直接命中

  • b.如果由冲突,则通过key.equals(k)去查找对应的entry

  • 如果为树,则在树中通过key.equals(k)查找,时间复杂度为o(logn);

  • 如果为链表,则在链表中通过key.equals(k查找),时间复杂度为o(n);

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

/**
 * Implements Map.get and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @return the node, or null if none
 */
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //如果桶的数量大于0并且待查找的key所在的桶的第一个元素不为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //检查第一个元素是不是要查的元素,如果是直接返回
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
         // 未命中
        if ((e = first.next) != null) {
           // 如果第一个元素是树节点,则按树的方式查找
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
             // 否则就遍历整个链表查找该元素
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

7、getTreeNode(int h, Object k)方法

 /**
         * Calls find for root node.
         */
        final TreeNode<K,V> getTreeNode(int h, Object k) {
        //从树的根节点开始查找
            return ((parent != null) ? root() : this).find(h, k, null);
        }
        
         final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
            TreeNode<K,V> p = this;
            do {
                int ph, dir; K pk;
                TreeNode<K,V> pl = p.left, pr = p.right, q;
                if ((ph = p.hash) > h)
                //左子树
                    p = pl;
                else if (ph < h)
                //左子树
                    p = pr;
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                //找到了直接返回
                    return p;
                else if (pl == null)
                //hash值相同但是key不同,左子树为空查右子树
                    p = pr;
                else if (pr == null)
                //右子树为空查左子树
                    p = pl;
                else if ((kc != null ||
                          (kc = comparableClassFor(k)) != null) &&
                         (dir = compareComparables(kc, k, pk)) != 0)
                         //// 通过compare方法比较key值的大小决定使用左子树还是右子树
                    p = (dir < 0) ? pl : pr;
                else if ((q = pr.find(h, k, kc)) != null)
                //// 如果以上条件都不通过,则尝试在右子树查找
                    return q;
                else
                  // 都没找到就在左子树查找
                    p = pl;
            } while (p != null);
            return null;
        }

8、hash函数的实现

在get和put的过程中,计算下标时,先对hashCode进行hash操作,然后再通过hash值进一步计算下标,如下图所示

static final int hash(Object key) {
    int h;
     // ^ :按位异或
    // >>>:无符号右移,忽略符号位,空位都以0补齐
    //其中n是数组的长度,即Map的数组部分初始化长度
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

我们可以看到,在 hashmap 中要找到某个元素,需要根据 key 的 hash 值来求得对应数组中的位置。如何计算这个位置就是 hash 算法。

前面说过,hashmap 的数据结构是数组和链表的结合,所以我们当然希望这个 hashmap 里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个。那么当我们用 hash 算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。 所以,我们首先想到的就是把 hashcode 对数组长度取模运算。这样一来,元素的分布相对来说是比较均匀的。

但是“模”运算的消耗还是比较大的,能不能找一种更快速、消耗更小的方式?我们来看看 JDK1.8 源码是怎么做的

简单来说就是:

*高16 bit 不变,低16 bit 和高16 bit 做了一个异或(得到的 hashcode 转化为32位二进制,前16位和后16位低16 bit和高16 bit做了一个异或)

*(n·1) & hash = -> 得到下标

9、拉链法导致的链表过深,为什么不用二叉查找树代替而选择红黑树?为什么不一直使用红黑树?

之所以选择红黑树是为了解决二叉查找树的缺陷:二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成层次很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋、右旋、变色这些操作来保持平衡。引入红黑树就是为了查找数据快,解决链表查询深度的问题。我们知道红黑树属于平衡二叉树,为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少。所以当长度大于8的时候,会使用红黑树;如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。

10、为什么链表的长度达到了8就会变成红黑树,而不是6,10,20的长度

HashMap在jdk 1.8后引入了红黑树的概念,表示若桶中的元素个数超过8时,会自动转化成红黑树;若桶中元素小于等于6时,树结构还原成链表形式.

原因:红黑树的平均查找长度是log(n),长度为8,查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。

选择6和8的原因是: 中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

11、对红黑树的见解

  • 1、每个节点非红即黑

  • 2、根节点总是黑色的

  • 3、如果节点是红色的,则它的子节点必须是黑色的(反之不一定)

  • 4、每个叶子节点都是黑色的空节点(NIL节点)

  • 5、从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)

12、如果 HashMap 的大小超过了负载因子(load factor)定义的容量怎么办?

HashMap 默认的负载因子大小为0.75。也就是说,当一个 Map 填满了75%的 bucket 时候,和其它集合类一样(如 ArrayList 等),将会创建原来 HashMap 大小的两倍的 bucket 数组来重新调整 Map 大小,并将原来的对象放入新的 bucket 数组中。这个过程叫作 rehashing。

13、重新调整 HashMap 大小存在什么问题吗?

重新调整 HashMap 大小的时候,确实存在条件竞争。

因为如果两个线程都发现 HashMap 需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来。因为移动到新的 bucket 位置的时候,HashMap 并不会将元素放在链表的尾部,而是放在头部。这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。多线程的环境下不使用 HashMap。

14、多线程会导致死循环(jdk 1.8之前),它是怎么发生的?

HashMap 的容量是有限的。当经过多次元素插入,使得 HashMap 达到一定饱和度时,Key 映射位置发生冲突的几率会逐渐提高。这时候, HashMap 需要扩展它的长度,也就是进行resize。

多线程put数据到HashMap中

public class HashMapInfiniteLoop {
    private static HashMap<Integer,String> map = new HashMap<>(2,0.75f);

    public static void main(String[] args) {
        map.put(3,"A");

        new Thread("thread1" ){
            @Override
            public void run() {
                map.put(7,"B");
                System.out.println(map);
            }
        }.start();


        new Thread("thread2"){
            @Override
            public void run() {
                map.put(5,"C");
                System.out.println(map);
            }
        }.start();
    }
}

jdk 1.8之前的put()方法

public V put(K key, V value) {
    ......
    // 计算 Hash 值
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    // 如果该 key 已被插入,则替换掉旧的 value(链接操作)
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    // 该 key 不存在,需要增加一个结点
    addEntry(hash, key, value, i);
    return null;
}

检查容量是否超标

void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    //查看当前的 size 是否超过了设定的阈值 threshold,如果超过,需要 resize
    if (size++ >= threshold)
        resize(2 * table.length);
}

新建一个更大尺寸的hash表,然后把数据从老的hash表中迁移到新的hash表中

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    ......
    // 创建一个新的 Hash Table
    Entry[] newTable = new Entry[newCapacity];
    // 将 Old Hash Table 上的数据迁移到 New Hash Table 上
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}


迁移的方法

void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    //下面这段代码的意思是:
    //  从OldTable里摘一个元素出来,然后放到NewTable中
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

该方法实现的机制就是将每个链表转化到新链表,并且链表中的位置发生反转,而这在多线程情况下是很容易造成链表回路,从而发生 get() 死循环。所以只要保证建新链时还是按照原来的顺序的话就不会产生循环(JDK 8 做了改进)

多线程情况下的transfer 1)、假设有两个线程

do {
    Entry<K,V> next = e.next; //  假设线程一执行到这里就被调度挂起了
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
} while (e != null);

而第二个线程执行完成了,于是就是下面这个样子

注意,因为 Thread1 的 e 指向了 key(3),而 next 指向了 key(7),其在线程二 rehash 后,指向了线程二重组后的链表。可以看到链表的顺序被反转

2)线程一被调度回来执行

  • 先是执行 newTalbe[i] = e;
  • 然后是 e = next,导致了 e 指向了 key(7)
  • 而下一次循环的 next = e.next 导致了 next 指向了 key(3)

3)线程一接着工作。把 key(7) 摘下来,放到 newTable[i] 的第一个,然后把 e 和 next 往下移

4)环形链接出现 e.next = newTable[i] 导致 key(3).next 指向了 key(7) 此时的 key(7).next 已经指向了 key(3), 环形链表就这样出现了

jdk 8的改进 JDK 8 中采用的是位桶 + 链表/红黑树的方式,当某个位桶的链表的长度超过 8 的时候,这个链表就将转换成红黑树 HashMap 不会因为多线程 put 导致死循环(JDK 8 用 head 和 tail 来保证链表的顺序和之前一样;JDK 7 rehash 会倒置链表元素),但是还会有数据丢失等弊端(并发本身的问题)。因此多线程情况下还是建议使用 ConcurrentHashMap

假如一个线程m.containsKey(k)为真,在还没执行m.get(k)的时候,k被另外一个线程给删除了,那么m.get(k)会返回null。如果允许null值的话,就会错误的判断为k还存在;因此不允许null值的话就可以正常的表示出当前的k是不存在的。所以在ConcurrentHashMap不应该有如下的写法,Key和Value不允许null值。 其实Value不允许null值就可以,Key为null似乎没什么影响。

Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
    next = e.next;
    if ((e.hash & oldCap) == 0) {
        if (loTail == null)
            loHead = e;
        else
            loTail.next = e;
        loTail = e;
    }
    else {
        if (hiTail == null)
            hiHead = e;
        else
            hiTail.next = e;
        hiTail = e;
    }
} while ((e = next) != null);
if (loTail != null) {
    loTail.next = null;
    newTab[j] = loHead;
}
if (hiTail != null) {
    hiTail.next = null;
    newTab[j + oldCap] = hiHead;
}

声明两个指针,维护两个链表,依次在末端添加新的元素。虽然解决了死循环问题,但还是会有其他问题,所以多线程还是尽量用ConcurrentHashMap。

这里比较难理解的是 (e.hash & oldCap) == 0 这一句,但是这个和解决死循环没什么关系。

这里的操作就是 (e.hash & oldCap) == 0 这一句,这一句如果是true,表明(e.hash & (newCap - 1))还会和 e.hash & (oldCap - 1)一样。因为oldCap和newCap是2的次幂,并且newCap是oldCap的两倍,就相当于oldCap的唯一一个二进制的1向高位移动了一位 , (e.hash & oldCap) == 0就代表了(e.hash & (newCap - 1))还会和e.hash & (oldCap - 1)一样。

比如原来容量是16,那么就相当于e.hash & 0x1111 (0x1111就是oldCap - 1 = 16 - 1 = 15),现在容量扩大了一倍,就是32,那么rehash定位就等于e.hash & 0x11111 (0x11111就是newCap - 1 = 32 - 1 = 31)现在(e.hash & oldCap) == 0就表明了e.hash & 0x10000 == 0,这样的话,不就是已知: e.hash & 0x1111 = hash定位值Value并且 e.hash & 0x10000 = 0那么 e.hash & 0x11111 不也就是原来的hash定位值Value吗?

那么就只需要根据这一个二进制位就可以判断下次hash定位在哪里了。将hash冲突的元素连在两条链表上放在相应的位置不就行了嘛。

为什么线程不安全:

HashMap 在并发时可能出现的问题主要是两方面:

  • 如果多个线程同时使用 put 方法添加元素,而且假设正好存在两个 put 的 key 发生了碰撞(根据 hash 值计算的 bucket 一样),那么根据 HashMap 的实现,这两个 key 会添加到数组的同一个位置,这样最终就会发生其中一个线程 put 的数据被覆盖
  • 如果多个线程同时检测到元素个数超过数组大小 * loadFactor,这样就会发生多个线程同时对 Node 数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给 table,也就是说其他线程的都会丢失,并且各自线程 put 的数据也丢失

二、ConcurrentHashMap源码解读

1、ConcurrentHashMap的key和value不能为null值

ConcurrentHashmap和Hashtable都是支持并发的,这样会有一个问题,当你通过get(k)获取对应的value时,如果获取到的是null时,你无法判断,它是put(k,v)的时候value为null,还是这个key从来没有做过映射。 HashMap是非并发的,可以通过contains(key)来做这个判断。 而ConcurrentHashMap在调用m.containsKey(key)和m.get(key),这两个方法都是没有加锁的,调用时候m可能被其他线程改变了。

if (m.containsKey(k)) {
   return m.get(k);
} else {
   throw new KeyNotPresentException();
}

2、ConcurrentHashMap的关键属性

ConcurrentHashMap在1.8中的实现,相比较1.7基本上全部都变掉了.而是启用了一种全新的CAS算法的方式实现,Node + CAS + Synchronized。数据结构沿用了与它同时期的HashMap版本的思想,底层依然由数组+链表+红黑树的方式思想。对于锁的粒度,调整为对每个数组元素加锁(Node),然后是定位节点的hash算法被简化了,这样带来的弊端是Hash冲突会加剧。因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。这样一来,查询的时间复杂度就会由原先的O(n)变为O(logN)。下面是其基本结构:

相关属性:

/**
*表初始化和调整控件大小。当为负时,

正在初始化或调整表的大小:初始化为-1,

否则-(1+活动调整大小线程的数目)。否则,

当表为空时,保留要在其上使用的初始表大小

创建,或默认为0。初始化后,保持

下一个要调整表大小的元素计数值
*/

private transient volatile int sizeCtl;

sizeCtl用于table[]的初始化和扩容操作,不同值的代表状态如下:

  • -1:table[]正在初始化。
  • -N:表示有N-1个线程正在进行扩容操作。

非负情况:

  • 如果table[]未初始化,则表示table需要初始化的大小。
  • 如果初始化完成,则表示table[]扩容的阀值,默认是table[]容量的0.75 倍。

private static finalint DEFAULT_CONCURRENCY_LEVEL = 16;

  • DEFAULT_CONCURRENCY_LEVEL:表示默认的并发级别,也就是table[]的默认大小。

private static final float LOAD_FACTOR = 0.75f;

  • LOAD_FACTOR:默认加载因子

static final int TREEIFY_THRESHOLD = 8;

  • TREEIFY_THRESHOLD:链表转红黑树的阀值,当table[i]下面的链表长度大于8时就转化为红黑树结构。

static final int UNTREEIFY_THRESHOLD = 6;

  • UNTREEIFY_THRESHOLD:红黑树转链表的阀值,当链表长度<=6时转为链表(扩容时)。

构造函数:

public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (initialCapacity < concurrencyLevel)   // Use at least as many bins
            initialCapacity = concurrencyLevel;   // as estimated threads
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        this.sizeCtl = cap;
    }

从上面代码可以看出,在创建ConcurrentHashMap时,并没有初始化table[]数组,只对Map容量,并发级别等做了赋值操作。相关节点:

  • Node:该类用于构造table[],只读节点(不提供修改方法)。
  • TreeBin:红黑树结构。
  • TreeNode:红黑树节点。
  • ForwardingNode:临时节点(扩容时使用)。

table

transient volatile Node<K,V>[] table; 装载Node的数组,作为ConcurrentHashMap的数据容器,采用懒加载的方式,直到第一次插入数据的时候才会进行初始化操作,数组的大小总是为2的幂次方。

nextTable

private transient volatile Node<K,V>[] nextTable;volatile Node<K,V>[] nextTable; //扩容时使用,平时为null,只有在扩容的时候才为非null

sizeCtl

private transient volatile int sizeCtl;该属性用来控制table数组的大小,根据是否初始化和是否正在扩容有几种情况: 当值为负数时:如果为-1表示正在初始化,如果为-N则表示当前正有N-1个线程进行扩容操作; 当值为正数时:如果当前数组为null的话表示table在初始化过程中,sizeCtl表示为需要新建数组的长度; 若已经初始化了,表示当前数据容器(table数组)可用容量也可以理解成临界值(插入节点数超过了该临界值就需要扩容),具体指为数组的长度n 乘以 加载因子loadFactor; 当值为0时,即数组长度为默认初始值。

sun.misc.Unsafe U

private static final sun.misc.Unsafe U;在ConcurrentHashMapde的实现中可以看到大量的U.compareAndSwapXXXX的方法去修改ConcurrentHashMap的一些属性。这些方法实际上是利用了CAS算法保证了线程安全性,这是一种乐观策略,假设每一次操作都不会产生冲突,当且仅当冲突发生的时候再去尝试。而CAS操作依赖于现代处理器指令集,通过底层CMPXCHG指令实现。CAS(V,O,N)核心思想为:若当前变量实际值V与期望的旧值O相同,则表明该变量没被其他线程进行修改,因此可以安全的将新值N赋值给变量;若当前变量实际值V与期望的旧值O不相同,则表明该变量已经被其他线程做了处理,此时将新值N赋给变量操作就是不安全的,在进行重试。而在大量的同步组件和并发容器的实现中使用CAS是通过sun.misc.Unsafe类实现的,该类提供了一些可以直接操控内存和线程的底层操作,可以理解为java中的“指针”。该成员变量的获取是在静态代码块中:

static {
     try {
         U = sun.misc.Unsafe.getUnsafe();
         .......
     } catch (Exception e) {
         throw new Error(e);
     }
 }

3、ConcurrentHashMap中关键内部类

Node

Node类实现了Map.Entry接口,主要存放key-value对,并且具有next域

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;

        Node(int hash, K key, V val, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.val = val;
            this.next = next;
        }

        public final K getKey()       { return key; }
        public final V getValue()     { return val; }
        public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
        public final String toString(){ return key + "=" + val; }
        public final V setValue(V value) {
            throw new UnsupportedOperationException();
        }

        public final boolean equals(Object o) {
            Object k, v, u; Map.Entry<?,?> e;
            return ((o instanceof Map.Entry) &&
                    (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
                    (v = e.getValue()) != null &&
                    (k == key || k.equals(key)) &&
                    (v == (u = val) || v.equals(u)));
        }

        /**
         * Virtualized support for map.get(); overridden in subclasses.
         */
        Node<K,V> find(int h, Object k) {
            Node<K,V> e = this;
            if (k != null) {
                do {
                    K ek;
                    if (e.hash == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                } while ((e = e.next) != null);
            }
            return null;
        }
    }

另外可以看出很多属性都是用volatile进行修饰的,也就是为了保证内存可见性。

TreeNode

树节点,继承于承载数据的Node类。而红黑树的操作是针对TreeBin类的,从该类的注释也可以看出,也就是TreeBin会将TreeNode进行再一次封装

   static final class TreeNode<K,V> extends Node<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;

        TreeNode(int hash, K key, V val, Node<K,V> next,
                 TreeNode<K,V> parent) {
            super(hash, key, val, next);
            this.parent = parent;
        }

        Node<K,V> find(int h, Object k) {
            return findTreeNode(h, k, null);
        }

        /**
         * Returns the TreeNode (or null if not found) for the given key
         * starting at given root.
         */
        final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) {
            if (k != null) {
                TreeNode<K,V> p = this;
                do  {
                    int ph, dir; K pk; TreeNode<K,V> q;
                    TreeNode<K,V> pl = p.left, pr = p.right;
                    if ((ph = p.hash) > h)
                        p = pl;
                    else if (ph < h)
                        p = pr;
                    else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
                        return p;
                    else if (pl == null)
                        p = pr;
                    else if (pr == null)
                        p = pl;
                    else if ((kc != null ||
                              (kc = comparableClassFor(k)) != null) &&
                             (dir = compareComparables(kc, k, pk)) != 0)
                        p = (dir < 0) ? pl : pr;
                    else if ((q = pr.findTreeNode(h, k, kc)) != null)
                        return q;
                    else
                        p = pl;
                } while (p != null);
            }
            return null;
        }
    }

TreeBin

这个类并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象。

static final class TreeBin<K,V> extends Node<K,V> {
         TreeNode<K,V> root;
         volatile TreeNode<K,V> first;
         volatile Thread waiter;
         volatile int lockState;
         // values for lockState
         static final int WRITER = 1; // set while holding write lock
         static final int WAITER = 2; // set when waiting for write lock
         static final int READER = 4; // increment value for setting read lock
         ......
 }

ForwardingNode

在扩容时才会出现的特殊节点,其key,value,hash全部为null。并拥有nextTable指针引用新的table数组。

static final class ForwardingNode<K,V> extends Node<K,V> {
        final Node<K,V>[] nextTable;
        ForwardingNode(Node<K,V>[] tab) {
            super(MOVED, null, null, null);
            this.nextTable = tab;
        }

        Node<K,V> find(int h, Object k) {
            // loop to avoid arbitrarily deep recursion on forwarding nodes
            outer: for (Node<K,V>[] tab = nextTable;;) {
                Node<K,V> e; int n;
                if (k == null || tab == null || (n = tab.length) == 0 ||
                    (e = tabAt(tab, (n - 1) & h)) == null)
                    return null;
                for (;;) {
                    int eh; K ek;
                    if ((eh = e.hash) == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                    if (eh < 0) {
                        if (e instanceof ForwardingNode) {
                            tab = ((ForwardingNode<K,V>)e).nextTable;
                            continue outer;
                        }
                        else
                            return e.find(h, k);
                    }
                    if ((e = e.next) == null)
                        return null;
                }
            }
        }
    }

4、CAS关键操作

在上面我们提及到在ConcurrentHashMap中会大量使用CAS修改它的属性和一些操作。因此,在理解ConcurrentHashMap的方法前我们需要了解下面几个常用的利用CAS算法来保障线程安全的操作。

tabAt

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
     return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
 }

该方法用来获取table数组中索引为i的Node元素。

casTabAt

 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);
 }

利用CAS操作设置table数组中索引为i的元素

setTabAt

static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
     U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
 }

该方法用来设置table数组中索引为i的元素

5、重点方法讲解

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;
            if (tab == null || (n = tab.length) == 0)
            //若table未创建,则初始化
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //table[i]后面无节点时,直接创建Node(无锁操作)
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // 如果当前正在扩容,则帮助扩容并返回最新table[]
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
            //在链表或者红黑树中追加节点
                V oldVal = null;
                //这里并没有使用ReentrantLock,说明synchronized已经足够优化了(1.7使用的ReentrantLock)
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                    //如果为链表结构
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //找到key,替换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;
                                //在尾部插入Node
                                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;
    }

从上面代码可以看出,put的步骤大致如下:

  • 参数校验。
  • 若table[]未创建,则初始化。
  • 当table[i]后面无节点时,直接创建Node(无锁操作)。
  • 如果当前正在扩容,则帮助扩容并返回最新table[]。
  • 然后在链表或者红黑树中追加节点。
  • 最后还回去判断是否到达阀值,如到达变为红黑树结构。

get操作:

 public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        //定位到table[]中的i
        int h = spread(key.hashCode());
        //若table[i]存在
        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;
    }

get方法的流程相对简单一点,从上面代码可以看出以下步骤:

  • 首先定位到table[]中的i。
  • 若table[i]存在,则继续查找。
  • 首先比较链表头部,如果是则返回。
  • 然后如果为红黑树,查找树。
  • 最后再循环链表查找。

ConcurrentHashMap的get操作上面并没有加锁。所以在多线程操作的过程中,并不能完全的保证一致性。这里和1.7当中类似,是弱一致性的体现。

size操作:

  public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 :
                (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                (int)n);
    }


 public long mappingCount() {
        long n = sumCount();
        return (n < 0L) ? 0L : n; // ignore transient negative values
    }

 final long sumCount() {
        CounterCell[] as = counterCells; CounterCell a;
        long sum = baseCount;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

JDK1.8中新增了一个mappingCount()的API。这个API与size()不同的就是返回值是Long类型,这样就不受Integer.MAX_VALUE的大小限制了。

两个方法都同时调用了,sumCount()方法。对于每个table[i]都有一个CounterCell与之对应,上面方法做了求和之后就返回了。从而可以看出,size()和mappingCount()返回的都是一个估计值。(这一点与JDK1.7里面的实现不同,1.7里面使用了加锁的方式实现。这里面也可以看出JDK1.8牺牲了精度,来换取更高的效率。)