HashMap

重要参数

  1. HashMap的初始化数组长度 **DEFAULT_INITIAL_CAPACITY** = 1 << 42^4=16 (为什么是2的n次方)
  2. 负载因子,当HashMap中总元素数量超出当前的**数组长度*负载因子**则需要扩容 **DEFAULT_LOAD_FACTOR **默认为0.75
  3. 链表转为红黑树的阈值,当链表长度达到阈值后则会从链表转换为红黑树 TREEIFY_THRESHOLD = 8 (原因:链表转换为红黑树是一个非常耗时的操作,因此需要阈值设定比较高,8满足泊松分布,哈希碰撞为8的概率已经非常小)
  4. 红黑树转为链表的阈值,当红黑树中节点的数量小于阈值后,从红黑树转换为链表 UNTREEIFY_THRESHOLD = 6 此参数仅仅用于扩容后进行判断,删除元素不根据此参数进行判断(原因:因为链表转红黑树的阈值设定是8,如果设定为7会导致红黑树和链表之间的频繁切换,因此设定为6
  5. 数组长度的最大值 MAXIMUM_CAPACITY = 1<<302^30=1073741824
  6. 转换红黑树的阈值,数组长度需到达一定的长度才会进行红黑树的转换,在数组长度不足时,会先进行数组的扩容 MIN_TREEIFY_CAPACITY = 64

HashMap原理

为什么使用数组+链表
  1. 在进行参数的加入时,会先进行 扰动函数的计算出key对应的数组下标 hash&(table.length-1),在出现Hash冲突时,则会通过**链表**的形式加入到后面,从而**解决哈希冲突**的问题。
  2. 可以用LinkedList代替数组,因为两者都是有序性的,但是因为在HashMap的使用过程中频繁的进行读取操作,而数组的读取时间复杂度是O(1)链表的时间复杂度是O(n),因此在使用过程中会使用数组而不适用链表
为什么数组的长度要保持二的倍数,以及扩容为什么也是二倍扩容
  1. 因为扰动函数的计算是hash&(table.length-1),利用的是key的hashcode进行取余操作得到
  2. 根据二进制中的计算,需要保证每一个二进制位为1,才能保证key的均匀分配
  3. 即保证table.length-1为的每个二进制位都是1,因此则数组的初始长度需要是2的倍数
为什么要有负载因子
  1. 负载因子用于计算阈值判断是否需要扩容,当HashMap中的元素到达 负载因子*数组长度时,则需要进行扩容
  2. 如果负载因子过大,出现哈希碰撞的情况就越多,因此数组每个下标中的元素也就越多,从而会导致查询速率较慢
  3. 如果负载因子过小,则会频繁的出现扩容的情况,扩容的操作涉及到申请新数组以及资源拷贝的过程,比较耗费时间,并且一定程度上需要占用更多的资源。
  4. 负载因子定为0.75属于折中方案

Put

JDK1.8

  1. 因为HashMap是懒加载形式,先判断数组是否初始化,如果是第一次调用则需要先调用 resize()方法进行HashMap的扩容
  2. 根据扰动函数 hash&(table.length-1)计算出对应数组的下标,如果数组下标中没有对应的元素则直接添加
  3. 如果下标的第一个元素与插入节点相同则直接进行覆盖,如果与第一个节点不相同则需要进行判断
  4. 如果下标节点为红黑树节点,则将节点插入到红黑树中
  5. 如果下标节点是链表节点,则从当前节点进行遍历,如果遇到相同的节点则将其覆盖,当遍历到链表的末尾时则将元素插入到末尾,如果插入后**链表的长度达到阈值 **TREEIFY_THRESHOLD = 8 此时会将链表转换为红黑树
  6. 转为红黑树的过程是,先调用treeifyBin将原有的单向链表的节点转换为双向链表,也就是转为TreeNode类,这个类继承了LinkedHashMap,包含的头尾指针和左右儿子和父节点,双向链表的主要作用是在扩容中使用
  7. 在转换为双向链表后,才会真正维护红黑树,红黑树主要作用是进行快速的查询
  8. 如果在HashMap中存在Key相同的值,则会在最后进行覆盖,覆盖后会返回原始节点的参数。
  9. 当put流程执行完毕后,最后判断HashMap中的实际大小是否大于 扩容的阈值 即(负载因子*数组长度)

image.png

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                  boolean evict) {
       Node<K,V>[] tab; Node<K,V> p; int n, i;
       if ((tab = table) == null || (n = tab.length) == 0)
// HashMap的懒加载策略,当执行put操作时检测Table数组初始化。
           n = (tab = resize()).length;
       if ((p = tab[i = (n - 1) & hash]) == null)
//通过``Hash``函数获取到对应的Table,如果当前Table为空,则直接初始化一个新的Node并放入该Table中。       
           tab[i] = newNode(hash, key, value, null);
       else {
           Node<K,V> e; K k;
           //进行值的判断: 判断对于是不是对于相同的key值传进来不同的value,若是如此,将原来的value进行返回
           if (p.hash == hash &&
               ((k = p.key) == key || (key != null && key.equals(k))))
               e = p;
           else if (p instanceof TreeNode)
          // 如果当前Node类型为TreeNode,调用 PutTreeVal 方法。
               e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
           else {
//如果不是TreeNode,则就是链表,遍历并与输入key做命中碰撞。 
               for (int binCount = 0; ; ++binCount) {
                   if ((e = p.next) == null) {

//如果当前Table中不存在当前key,则添加。
                       p.next = newNode(hash, key, value, null);
                       if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st

//超过了``TREEIFY_THRESHOLD``则转化为红黑树。
                           treeifyBin(tab, hash);
                       break;
                   }
                   if (e.hash == hash &&
                       ((k = e.key) == key || (key != null && key.equals(k))))            
//做命中碰撞,使用hash、内存和equals同时判断(不同的元素hash可能会一致)。
                       break;
                   p = e;
               }
           }
           if (e != null) { // existing mapping for key
           //如果命中不为空,更新操作。
               V oldValue = e.value;
               if (!onlyIfAbsent || oldValue == null)
                   e.value = value;
               afterNodeAccess(e);
               return oldValue;
           }
       }
       ++modCount;
       if (++size > threshold)
       //扩容检测!
           resize();
       afterNodeInsertion(evict);
       return null;
   }

JDK1.7

  1. 因为HashMap是懒加载形式,先判断数组是否初始化,如果是第一次调用则需要先调用 resize()方法进行HashMap的扩容
  2. 根据扰动函数 hash&(table.length-1)计算出对应数组的下标,如果数组下标中没有对应的元素则直接添加
  3. 如果下标的第一个元素与插入节点相同则直接进行覆盖,如果与第一个节点不相同则需要进行判断
  4. 如果下标节点是链表节点,则从当前节点进行遍历,如果遇到相同的节点则将其覆盖,当遍历到链表的末尾时则将元素插入到末尾
  5. 如果在HashMap中存在Key相同的值,则会在最后进行覆盖,覆盖后会返回原始节点的参数。
  6. 当put流程执行完毕后,最后判断HashMap中的实际大小是否大于 扩容的阈值 即(负载因子*数组长度)

扩容过程

JDK1.7

  1. put过程中,先判断是否达到阈值(负载因子*数组长度),如果到达阈值并且发生哈希冲突则进行扩容,扩容后再添加新元素
  2. 创建新数组,一般新数组长度为原数组的2的次方倍
  3. 双层循环遍历原数组和链表中的每一个元素,需要重新计算每个元素的Hash值,再计算下标,最终将该元素利用头插法插入到数组的新下标中
  4. 当多线程环境下,多个线程同时扩容时会出现死循环问题

JDK1.8

  1. JDK1.8中,先进行元素的添加,元素添加成功后,最后判断总元素个数是否达到阈值,判断是否进行扩容
  2. 遍历数组,如果当前是单个节点,则直接获取该节点的hash值计算出下标进行填入
  3. 如果是链表节点,不需要像JDK1.7中重新计算哈希值,新元素的插入位置只有两种可能,即当前位置和当前位置+旧数组长度,根据**(e.hash & oldCap) == 0公式**,计算出插入到那个位置,即新增位数是1还是0。遍历单链表通过尾插法获取两个结果单链表,分别将新数组的下标位置赋值到单链表。当扩容后的链表长度大于等于8时,则会转化为红黑树,如果小于等于6则会转为单链表。
  4. 当多线程环境下,多个线程同时进行扩容,由于是直接将 转换红黑树和链表的返回值赋值给数组下标,因此会导致数据覆盖而导致丢失的问题
/**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
    //先将老的Table取别名,这样利于后面的操作。
        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) {
            // 扩容 阙值为 Int类型的最大值,这种情况很少出现
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }

            //表示 old数组的长度没有那么大,进行扩容,两倍(这里也是有讲究的)对阙值也进行扩容
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //表示之前的容量是0 但是之前的阙值却大于零, 此时新的hash表长度等于此时的阙值
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
        //表示是初始化时候,采用默认的 数组长度* 负载因子
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //此时表示若新的阙值为0 就得用 新容量* 加载因子重新进行计算。
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        // 开始对新的hash表进行相对应的操作。
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
        //遍历旧的hash表,将之内的元素移到新的hash表中。
            for (int j = 0; j < oldCap/***此时旧的hash表的阙值*/; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                //表示这个格子不为空
                    oldTab[j] = null;
                    if (e.next == null)
                    // 表示当前只有一个元素,重新做hash散列并赋值计算。
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                    // 如果在旧哈希表中,这个位置是树形的结果,就要把新hash表中也变成树形结构,
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                    //保留 旧hash表中是链表的顺序
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {// 遍历当前Table内的Node 赋值给新的Table。
                            next = e.next;
                            // 原索引
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 原索引+oldCap
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 原索引放到bucket里面
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 原索引+oldCap 放到bucket里面
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

Get

  1. 首先利用扰动函数计算key对应的下标
  2. 如果只有一个节点命中则直接返回,直接复杂度o(1)
  3. 如果节点下为链表,则查询的时间复杂度为O(n)
  4. 如果节点下为红黑树,则查询的时间复杂度为O(logn)

线程不安全存在的问题

多线程下扩容导致死循环

// 扩容 
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) { // A
         while(null != e) { 
                //1. 当线程A执行到此处并阻塞  此时e指向e.next
               Entry<K,V> next = e.next; 
               if (rehash) {
                       e.hash = null == e.key ? 0 : hash(e.key); 
               }
               int i = indexFor(e.hash, newCapacity); 
             //2.线程B执行完成扩容流程 此时e.next 指向e
            //3.此时执行e.next = new Table[i] 这个新Table因为尾插的关系是 当为两个节点时就是之前的e.next
             //4.此时会出现死循环问题
               e.next = newTable[i];
               newTable[i] = e;
               e = next; } 
          }
 }
  1. JDK1.7中扩容使用头插法进行扩容,在多线程环境下,如果有多个线程同时触发扩容操作,则会出现死锁情况。
  2. 当线程A和线程B同时进行扩容时,同时指向第一和第二个节点,当此时线程B因为某种原因而阻塞,当线程A完成扩容后,线程B进行扩容。但因为JDK1.7中使用头插法进行扩容,此时原本的第一个和第二个节点变成逆序,即第二个节点指向第一个节点,因此在线程B进行扩容时,线程B也会进行头插法进行扩容,此时第一个节点又会指向第二个节点,从而导致死循环

多线程put参数丢失

  1. 当多线程put过程中,在发生扩容时,节点移动到了新的Hash下标下,导致另外的线程在原来的Hash下标中找不到对应的信息

多线程put非null参数,取出时为null

  1. 多次put中,可能会导致参数被覆盖
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值