• HashMap知识点


    1.基本数据结构

     

      1. JDK1.7 数组 + 链表

      2. JDK1.8 数组 + (链表 | 红黑树)

    2.树化与退化

      1.树化意义

        1.红黑树用来避免Dos攻击,防止链表过长时性能下降,树化应该是偶然情况,是保底策略.

        2.hash 表的查找,更新的时间复杂度是 O(1),而红黑树的查找,更新的时间复杂度是 O(log2⁡n ),TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表

        3.hash 值如果足够随机,则在 hash 表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006,树化阈值选择 8 就是为了让树化几率足够小

      2.树化规则

        1.当链表长度超过树化阈值8时,先尝试扩容来减少链表长度,如果数组容量已经 >= 64,才会进行树化.

      3.退化规则

        1. 情况1:在扩容时如果拆分树时,树元素 <= 6 则会退化链表

        2. 情况2:在 remove 树节点时若 root 根节点,根节点的左右子节点或者孙节点只要有一个为 null 也会退化为链表.

    3.索引计算

      1.索引计算方法

        1.首先,计算对象的hashCode()

        2.再进行调用HashMap的hash()方法进行二次哈希 (二次哈希是为了综合高位数据,让哈希分布更为均匀)

        3.最后 哈希结果 & (数组容量 - 1) 得到索引位置

      2.数组容量为何是2的 n 次幂

        1.计算索引时,如果是2的n次幂可以使用位于运算替代取模,效率更高.

        2.扩容时 hash & 旧容量 == 0的元素留在原来位置,否则新位置 = 旧位置 + 旧容量

        3.上述都是为了配合容量为2的 n 次幂时的优化手段,例如 HashTable的容量就不是2的 n 次幂,并不能说哪种设计更好.

    4. put 与 扩容

      1. put 流程

        1.HashMap 是懒惰创建数组的,首次使用才创建数组

        2.计算索引(桶下标)

        3.如果桶下标还没人占用,创建 Node 占位返回

        4.如果桶下标已经有人占用

          1.已经是 TreeNode 走红黑树的添加或更新逻辑

          2.是普通 Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑

        5.返回前检查容量是否超过阈值,一旦超过进行扩容

      2. 1.7 与 1.8 的区别

        1. 表插入节点时,1.7是头插法,1.8是尾插法

        2. 1.7是大于等于阈值且没有空位时才扩容,而1.8是大于阈值就扩容

        3. 1.8在扩容计算Node索引时会优化

      3. 扩容加载因子 (factor) 为何默认是0.75f

        1. 在空间占用和查询时间之间取得比较好的权衡

        2. 大于这个值,空间节省了,但是链表就会比较长,影响性能

        3. 小于这个值,冲突减少了,但是扩容更频繁,空间占用更多

    5.并发问题

      1.扩容死链 (1.7会存在)

        1.7 源码如下:

    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
    • e 和 next 都是局部变量,用来指向当前节点和下一个节点

    • 线程1(绿色)的临时变量 e 和 next 刚引用了这俩节点,还未来得及移动节点,发生了线程切换,由线程2(蓝色)完成扩容和迁移

       

      线程2 扩容完成,由于头插法,链表顺序颠倒。但线程1 的临时变量 e 和 next 还引用了这俩节点,还要再来一遍迁移

      第一次循环

      • 循环接着线程切换前运行,注意此时 e 指向的是节点 a,next 指向的是节点 b

      • e 头插 a 节点,注意图中画了两份 a 节点,但事实上只有一个(为了不让箭头特别乱画了两份)

      • 当循环结束是 e 会指向 next 也就是 b 节点

      • 第二次循环

        • next 指向了节点 a

        • e 头插节点 b

        • 当循环结束时,e 指向 next 也就是节点 a

         

         

        第三次循环

        • next 指向了 null

        • e 头插节点 a,a 的 next 指向了 b(之前 a.next 一直是 null),b 的 next 指向 a,死链已成

        • 当循环结束时,e 指向 next 也就是 null,因此第四次循环时会正常退出

        •  

  • 相关阅读:
    XAF 有条件的对象访问权限
    XAF 顯示 UnInplace Report(設置自定義條件顯示報表,不是根據選擇ListView記錄條件顯示報表)
    XAF 如何自定义PivotGrid单元格显示文本?
    XAF 如何布局详细视图上的按钮
    XAF How to set size of a popup detail view
    XAF Delta Replication Module for Devexpress eXpressApp Framework
    XAF 帮助文档翻译 EasyTest Basics(基础)
    XAF 用户双击ListView记录时禁止显示DetailView
    XAF How to enable LayoutView mode in the GridControl in List Views
    XAF 如何实现ListView单元格批量更改?
  • 原文地址:https://www.cnblogs.com/xuxiaobai13/p/15458083.html
Copyright © 2020-2023  润新知