Java Collection集合源码详解 —— Map(2)

140 阅读11分钟

Java Collection集合源码详解 —— Map(2)

  • 作者:shiwyang 今天发现HashMap居然还有红黑树退化的现象,原来是还有几个方法没看完,今天在这里开第二篇补充一下

红黑树退化

  • 在HashMap中,红黑树的结点个数当小于 6 的时候,会使得红黑树退化为原来的链表(具体原因在源码注释里面写了,利用泊松分布算出来的 8扩容 6退化),红黑树会在两种情况中判断是否需要退化
    • 数组扩容时(数组扩容时,内部所有元素都需要重新确定索引值,因此一个数组中的桶元素可能会减少)
    • 移除元素时(移除元素可能使桶内的元素小于6个,红黑树发生退化)
  • 在内部有涉及树的退化的两个函数
    • split() 数组扩容时候使用的
    • removeTreeNode() 移除元素的时候使用的
数组扩容的红黑树退化
  • 学习数组扩容时的红黑树退化,首先就应该学习HashMap是如何扩容的,具体到代码上就是 resize() 方法
  • 在学习红黑树的退化之前,首先会遇到一个链表元素更新索引的问题,这个问题可以说是红黑树分裂和退化的基础,在了解了这个基础之上,我们才能够全面的认识红黑树的退化过程

resize()

  • 最好先自己先研究一遍代码,看不懂的地方先看我下方的代码注释,还是看不懂再看上面的文字,这个又复杂又神奇的,很有意思!

  • resize函数是HashMap的扩容方法,在扩容的时候有两种情况

    • 原table为空
    • 原table内有元素
  • 这个函数开头定义的每一个中间元素都是有意义的,每一个都不能缺

    • oldTable : 用来将原来的table先保存起来,将table空间扩展之后,再将原来的元素迁移过去,有点类似与Arrays里面的copyof()
    • oldCap :用来记录原数组的大小,后面在计算桶元素新索引的时候,需要用到这个数
    • oldThr: 用来记录原扩容常数,用来计算新扩容常数和判断原数组状态的
    • newCap、newThr: 用来保存新的数组大小 新的扩容常数,这两个变量可能会遇到超过范围的情况,超过范围有一个默认极大值,所以也需要单独拿出来
  • 当原数组为空时,扩容返回即可

  • 当原数组不为空时,扩容后数组内的桶都需要重新判断,桶有两种情况

    • 链表:需要将链表内所有的元素重新计算索引值
    • 红黑树:将红黑树使用spilt()重新分配,可能会涉及树的退化(重点)这个内容比较多,放在后面
  • 当原数组位置的桶结构是一个链表时,链表内的元素计算索引之后可能会有两种情况:1)原位 2)原位 + 原数组大小

    • 这里首先要了解,每一个元素是怎么算出这个索引值的?它是通过hash值和(Hash表数组长度 - 1)进行与运算(e.hash & (newCap - 1))可以得到一个不大于数组长度最大值的数,作为索引。列一个简单的计算过程复习一下与运算
    与运算过程(同1得1)例1例2
    hash值1111101010011010
    HashMap数组长度 - 101111(16 -1)01111(16-1)
    结果01010(十进制10)01010(十进制10)
    • 所以上面两个例子在数组内都是一样的下标,在扩容的时候,进行e.hash & oldCap,实际上对比的就是最高位的差别,对比一位的差别,只可能会出现两种情况,因此也就可以分为高位和原位两种情况
    • 至于为什么不用上面的计算方法呢,是因为上面的计算方法得到的是一个具体的索引值,而这种方法能够得到的是易于判断的数(0和其他)
    与运算过程(同1得1)例1例2
    hash值1111101010011010
    HashMap数组长度1000010000
    结果10000(高位)0(原位)
final Node<K,V>[] resize() {
  	// 用一个oldTab 先把目前的table 保存起来
    Node<K,V>[] oldTab = table;
  	// 原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;
        }
      	// 设定新的数组大小 和新扩容常数(计算出的数组大小必须小于int极值,还必须大于默认数组大小:10,如果没有,需要往下)
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
  	// 原数组为空 原扩容常数大于0时
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
  	// 原数组为空 原扩容常数也为0 ,就是正常第一次添加元素的情况
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
  	// 当新扩容常数为0时
  	// 这个情况出现时,就是当原数组不为空,但是扩容之后的数组又超过int极值的情况
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
  	// 经过上方一系列的判断,可以最终确定 newCap newThr两个值
  	// 下方正式进入扩容阶段
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
  	// 建立一个新的 Node[]数组表,大小为newCap
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  	// table 完成扩容 但是这时候原table的元素还在oldTab里面
    table = newTab;
  	// 当原table不为空的时候,将老的table元素迁移到table里面
  	// 如果原table为空的时候,就可以直接返回了,这种情况一般是在第一次添加元素的时候遇到
    if (oldTab != null) {
      	// 遍历老的table
        for (int j = 0; j < oldCap; ++j) {
          	// 保存遍历数组的头节点,使用Node对象来存储
          	// 但是这个元素既可能是一个链表,用Node对象存储,也可能是红黑树,用TreeNode存储
          	// 但是TreeNode 也可以用Node 表示,称之为向上转型,多态的概念
            Node<K,V> e;
          	// 取出数组的头节点
            if ((e = oldTab[j]) != null) {
              	// 清空原table内的元素,应该是为了GC
                oldTab[j] = null;
              	// 如果只有一个元素
                if (e.next == null)
                  	// e.hash & (newCap - 1) 这个计算式子可以算出新的索引位置,可能会和原索引值不同
                  	// 可能会在原位置,或者原位置 + 数组长度位置,具体原因也在这个计算式子里面,用& 运算之后的得出的结果,非常神奇
                    newTab[e.hash & (newCap - 1)] = e;
              	// 如果e是TreeNode (instanceof 当对象是右边类或子类所创建对象时 返回true,向下转型,多态的概念)
                else if (e instanceof TreeNode)
                  	// 将树中的每一个元素重新放到他们对应的位置,这个函数里面就涉及到树的退化过程
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
              	// 如果e是一个链表表头,且不止有一个元素时
                else { // preserve order
                  	// 设置了两条新的链条 loHead hiHead
                  	// 扩容后,数组内桶的每一个元素都需要重新计算一次索引值,确定定位位置
                  	// 而同一个桶内的所有数据,在重新计算hash值之后,只有可能是在原位置或者(原位置 + 原数组长度)的地方
                  	// 因为在和扩容常数-1的与运算中,只会涉及原位置 + 1位的计算,+1位的情况下只能有两种结果,这里我很难用文字表达出来,TODO:可能会在下面列一个计算索引的过程
                  	// 所以在重新计算索引值只可能会出现两种情况的基础之上,只需要设置两个数组链,在全部计算完成之后,将整个链表加入对应的位置
                  	// 算出新索引的最大用处是,可以降低hash冲突,减少桶深度,让原数组的各个桶的元素都均匀的分布在新的数组中,提高了搜索速度,提高了数组利用率
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                  	// 遍历链表的临时变量
                    Node<K,V> next;
                  	// 循环判断每一个元素的索引,加入对应的链表
                    do {
                        next = e.next;
                      	// hash和原数组大小进行与运算之后,就会在原数组最高位进行判断,会出现0和不等于0 两种情况,分别代表放在原位链表和高位链表
                      	// 1)放在原位链表
                        if ((e.hash & oldCap) == 0) {
                          	// 当链表还为空的情况
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                      	// 2) 放在高位链表
                        else {
                          	// 当链表还为空的情况
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                  	// 判断完原链表内所有元素的位置之后,生成两个链表,将链表放在对应的位置
                  	//1) 原位链表
                    if (loTail != null) {
                      	// 结束链表末端
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                  	// 2) 高位链表
                    if (hiTail != null) {
                      	// 结束链表末端
                        hiTail.next = null;
                      	// j + oldCap:原位+原数组长度
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
  	// 完成扩容!
    return newTab;
}

虽然刚研究完上面那么一大堆,但其实对于红黑树退化只能算是前置知识,在扩容的时候,如果是红黑树,红黑树内的元素也会重定位,所以也有一个大概的过程,在重定位之后,发生数量不足的情况才会到我们要讨论的红黑树退化的过程,下面就详细的研究一下split这个函数的源码。

split()

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;
  	// 根据上面链表重定位的情况,红黑树重新定位的原理也是一样的,除了可能会触发红黑树退化
    // Relink into lo and hi lists, preserving order
  	// 使用两个红黑树来记录 loHead  hiHead
    TreeNode<K,V> loHead = null, loTail = null;
    TreeNode<K,V> hiHead = null, hiTail = null;
  	// 两个变量分别记录树分裂后,各个树的大小,如果小于 6 就触发红黑树退化
    int lc = 0, hc = 0;
  	// 这个和链表分裂的情况基本一直
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        next = (TreeNode<K,V>)e.next;
        e.next = null;
        if ((e.hash & bit) == 0) {
            if ((e.prev = loTail) == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;
          	// 记录loTree 的元素数量
            ++lc;
        }
        else {
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
          	// 记录hiTree 的元素数量
            ++hc;
        }
    }

    if (loHead != null) {
      	// 假如 loTree 的元素数量 < UNTREEIFY_THRESHOLD(6)
        if (lc <= UNTREEIFY_THRESHOLD)
          	// 触发untreeify使红黑树退化
            tab[index] = loHead.untreeify(map);
        else {
          	// loTree 为原位
            tab[index] = loHead;
            if (hiHead != null) // (else is already treeified)
              	// 红黑树生成函数
                loHead.treeify(tab);
        }
    }
    if (hiHead != null) {
     	 	// 假如 hiTree 的元素数量 < UNTREEIFY_THRESHOLD(6)
        if (hc <= UNTREEIFY_THRESHOLD)
          	// loTree 为原位 + oldCap
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
              	// 红黑树生成函数
                hiHead.treeify(tab);
        }
    }
}

untreeify()

这个就是真正的红黑树退化的函数,但其实红黑树的退化过程很简单,只需要将TreeNode的向上转型为Node就行。

  • 把TreeNode 转化为 Node
  • 把Node链接起来
final Node<K,V> untreeify(HashMap<K,V> map) {
    Node<K,V> hd = null, tl = null;
    for (Node<K,V> q = this; q != null; q = q.next) {
      	// 向上转型
        Node<K,V> p = map.replacementNode(q, null);
      	// 链接链表
        if (tl == null)
          	// 头结点
            hd = p;
        else
          	// 链接每一个结点的next结点
            tl.next = p;
        tl = p;
    }
  	// 返回头节点
    return hd;
}
移除元素的红黑树退化
  • 判断要删除的元素是否属于红黑树,如果是,先获取到树结点getTreeNode
  • 获取到树节点,之后删除树结点removeTreeNode

removeNode()

final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
          	// 如果要删除的数组位置上的桶是红黑树结构的元素
            if (p instanceof TreeNode)
              	// 获得树结点内指定要删除的元素
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
      	// 如果找到了需要删除的元素
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
          	// 并且是红黑树元素
            if (node instanceof TreeNode)
              	// 执行removeTreeNode函数
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

removeTreeNode()

  • TODO:这个涉及红黑树的部分实在太复杂了,我还没搞懂,只能看懂简单逻辑
  • 先找到需要删除的元素
  • 找到删除的元素之后看看删除完还能不能保持红黑树的结构
    • 能 -> 平衡红黑树结构 判断红黑树是否成立 返回二叉树根结点
    • 不能 -> untreeify() 返回链表根结点
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                          boolean movable) {
    int n;
    if (tab == null || (n = tab.length) == 0)
        return;
    int index = (n - 1) & hash;
    TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
    TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
    if (pred == null)
        tab[index] = first = succ;
    else
        pred.next = succ;
    if (succ != null)
        succ.prev = pred;
    if (first == null)
        return;
    if (root.parent != null)
        root = root.root();
  	// 假如树空了,就untreeify
    if (root == null || root.right == null ||
        (rl = root.left) == null || rl.left == null) {
        tab[index] = first.untreeify(map);  // too small
        return;
    }
    TreeNode<K,V> p = this, pl = left, pr = right, replacement;
    if (pl != null && pr != null) {
        TreeNode<K,V> s = pr, sl;
        while ((sl = s.left) != null) // find successor
            s = sl;
        boolean c = s.red; s.red = p.red; p.red = c; // swap colors
        TreeNode<K,V> sr = s.right;
        TreeNode<K,V> pp = p.parent;
        if (s == pr) { // p was s's direct parent
            p.parent = s;
            s.right = p;
        }
        else {
            TreeNode<K,V> sp = s.parent;
            if ((p.parent = sp) != null) {
                if (s == sp.left)
                    sp.left = p;
                else
                    sp.right = p;
            }
            if ((s.right = pr) != null)
                pr.parent = s;
        }
        p.left = null;
        if ((p.right = sr) != null)
            sr.parent = p;
        if ((s.left = pl) != null)
            pl.parent = s;
        if ((s.parent = pp) == null)
            root = s;
        else if (p == pp.left)
            pp.left = s;
        else
            pp.right = s;
        if (sr != null)
            replacement = sr;
        else
            replacement = p;
    }
    else if (pl != null)
        replacement = pl;
    else if (pr != null)
        replacement = pr;
    else
        replacement = p;
    if (replacement != p) {
        TreeNode<K,V> pp = replacement.parent = p.parent;
        if (pp == null)
            root = replacement;
        else if (p == pp.left)
            pp.left = replacement;
        else
            pp.right = replacement;
        p.left = p.right = p.parent = null;
    }

    TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);

    if (replacement == p) {  // detach
        TreeNode<K,V> pp = p.parent;
        p.parent = null;
        if (pp != null) {
            if (p == pp.left)
                pp.left = null;
            else if (p == pp.right)
                pp.right = null;
        }
    }
    if (movable)
        moveRootToFront(tab, r);
}

其他的HashMap的源码在前面HashSet里面已经写过了。

HashMap的死循环问题

疫苗:Java HashMap的死循环 | 酷 壳 - CoolShell

这个链接比较清晰的写了造成死循环的情况

我认为关键的情况就是在并发的过程中造成的不可重复读现象