HashMap的数据结构浅析

作者: 椰子奶糖 | 来源:发表于2019-07-27 23:52 被阅读10次
    HashMap是非线程安全的。而HashMap的线程不安全主要体现在resize时的死循环

    HashMap工作原理

    • HashMap数据结构
      常用的底层数据结构主要有数组和链表。数组存储区间连续,占用内存较多,寻址容易,插入和删除困难。链表存储区间离散,占用内存较少,寻址困难,插入和删除容易。
    • HashMap要实现的是哈希表的效果,尽量实现O(1)级别的增删改查。它的具体实现则是同时使用了数组和链表,可以认为最外层是一个数组,数组的每个元素是一个链表的表头。


      HashMap数据结构

    HashMap的寻址方式

    • 对于新插入的数据或者待读取的数据,HashMap将Key的哈希值对数组长度取模,结果作为该Entry在数组中的index。因此数组的长度必须时2的N次方,实际中,HashMap会自动通过Integer.highestOneBit算出比指定整数大的最小的N值。
            public static int highestOneBit(int i) {
              i |= (i >>  1);
              i |= (i >>  2);
              i |= (i >>  4);
              i |= (i >>  8);
              i |= (i >> 16);
              return i - (i >>> 1);
            }
    
    • 碰撞:由于数组的长度是有限的,所以不管Hash()方法和Equals()方法写得如何严谨,始终不能完全避免碰撞的发生(Hash值算出来的Index相同,需要放在数组的同一个位置),碰撞发生后,我们就只能添加一个链表子节点,但是这无疑会降低查找效率(找到Index还要遍历链表。。。)
    • 加载因子:默认是0.75,当数组的被占空间>=0.75的时候,HashMap就会进行扩容(变为两倍),扩容之后数组中的所有元素进行重排序(算出来的Index可能不同),从而减少数组下链表的长度,提高查找效率。

    Java8 中的升级

    • Java8 中的HashMap采用了数组+链表+红黑树(类似于数据结构中的平衡二叉树)的模式(当链表长度<8时仍然采用链表的形式,>8时由链表的数据结构转换成红黑树的数据结构)


      Java8HashMap.png

    JDK1.7中线程不安全的HashMap

    • 上文讲到占用空间超过加载因子的值时,就会自动扩容,这时HashMap中的元素或重新计算排序,这显然是不能保证线程安全的,而且在多线程并发调用时,可能出现死循环。
    • 首先:先给出resize的核心代码:
    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;  
                } 
            }  
        }  
    
    • 在多线程调用中,可能会产生这种情况:两个线程同时认为HashMap需要rehash,这里就有一种可能性,这两个线程在调用的时候 线程访问.png
    • 当两个线程痛时执行的时候,可能有一个会被挂起,等待另一个结束后在继续执行,这时问题就产生了;


      HashMap线程不安全图解.png
    • 产生死循环的原因是:while(null != e)这句判断,在第一次执行的时候,B原本->null,于是重新计算到B的时候B->null,判断体就结束执行,于是就扩容成功
    • 当第二个线程执行扩容的时候,内存中是B->A ,是的,条件成立,而原本的条件(线程2以为的条件)是A->B 这里,两个条件同时成立,后果就是。。。一直循环下去A->B->A->B......
      • 具体过程是A->B,执行while,将A头插到数组,指针指向下一个,B->A,条件成立,将B头插如数组,指针指向下一个,A->B........

    JDK1.8的优化

    • 在JDK1.8中采用了尾插法,可以有效避免上述这种死循环的情况。

    以上就是我目前的理解,还有一种使用迭代器时的fast-fail,以后有机会更新。

    相关文章

      网友评论

        本文标题:HashMap的数据结构浅析

        本文链接:https://www.haomeiwen.com/subject/faqlrctx.html