HashMap的一些原理(同时分析源码,英文)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/CelloRen/article/details/78705709
                            HashMap的原理和问题:
                                              First.      基本原理
                                              Second. HashMap的初始长度,以及原因

                                              Third.     loadFactor以及resize

                                              fourth.    高并发下的死锁问题

                                             

First,some fundamental theory:

     1.hashMap存储的是key-value的键值对集合。

       每一个键值对也叫Entry,Entry分散分布在一个数组里。比如说这里的hashMap的一个实例x对应的数组a长度为6.

     2.put和get方法:

       (1)put

       比如说x.put("apple",0),<apple,0>就是一个Entry,记作entryOne。

       在放入entryOne之前,我们要知道究竟放在数组a的哪个位置。在这里要先计算index=Hash("apple"),我们暂时不管怎么计算,如果最终index=2的话,那么entryOne就放在a[2]处。

       那么就有一个问题,因为a的长度有限,再放入别的Entry的时候会不会和前面所放的冲突,答案是会的。此时怎么解决这个问题呢?

       java选择开辟链表法,也就是说在发生冲突的位置用链表的形式存储index为该位置的Entries.比如说x.put("orange",1),index=Hash("orange")=2,把<orange,2>记作entryTwo。此时a[2]处的头节点为entryTwo,然后entryTwo指向entryOne。至于为什么后放入的要放在头部(头部插入),这是因为往往新插入的被访问的几率更高。

       (2)get

       我们根据key,在hashMap x中找到相应的Entry。

       同理,比如找“apple”,同样计算index=Hash("apple")=2,但就在a[2]处我们发现了一个链表,依次遍历直到找到Entry y,使得y.key==apple,或者没找到,返回null。


Second,the initial length of capacity and the reason

       java的hashMap的初始长度为16,2的4次方

     /*** The default initial capacity - MUST be a power of two.*/
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    以上是源码的初始化,但为什么说初始的容量必须是2的幂次呢?

    我们上面提到计算index=Hash("key"),具体我们是怎么计算的呢?

    实际上,index=Hash("key")=HashCode("key")%length.这里HashCode("key")是key对应的哈希码,至于怎么产生的后面再介绍。

    那么对于index=Hash("key")的映射函数,我们希望这个hash函数尽量分布均匀。

    另外,由于取模运算效率极低,hashMap的开发者使用的是位运算。

    有:index=HashCode(key)&(length-1);

    为了具体说明,比如说apple的hashCode为xxxxxxxxxxxxx1001(高位值不影响结果),hashMap的length初始值为16,length-1=15=1111。则,结果为1001。其实就是保留hashCode的低位,由于是&运算,只有length-1为全1时保留的最多,结果最均匀。

    我们可以假设length=10,length=9=1001,xxxxxxxxxxxxx1111xxxxxxxxxxxxx1001

结果都为1001,实际上x11x的情况根本不会出现。这会导致,hashMap的一些位置根本就没有entry,而别的位置挤满了entry。  


 Third.     loadFactor  and resize the hashMap

   我们知道哪怕再平均的hash映射函数,只要插入的数据够多,就会影响速度,这个时候就要扩大hashMap,我们把扩大的过程叫做resize。另一方面,什么时候我们决定resize呢?换句话说,插入的数据多到什么程度我们resize hashMap呢?这取决于loadFactor,在java的hashMap中取其为0.75。

   /*** The load factor used when none specified in constructor.*/
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

至于为什么是0.75,开发者在一开始就给出了解释:

  As a general rule, the default load factor (.75) offers a good
  tradeoff between time and space costs.  Higher values decrease the
  space overhead but increase the lookup cost (reflected in most of
  the operations of the HashMap class, including
 get and put).

这是时间和空间代价的平衡点。

而resize所做的操作则是,在插入占到0.75以上时,HashMap使用新数组代替旧数组,对原有的元素根据hash值重新就算索引位置,重新安放所有对象。

要注意!之前的链表在resize之后,相当于反序了,因为resize遍历的过程从头到尾将entry放入新的hashMap中,而且是头部插入。

 

fourth.  deadlock caused by high concurrency 

     为什么高并发下hashMap容易出现死锁?就是resize的问题,当hashMap处于阈值,也就是即将被扩容,如果此时有两个进程同时对其resize,因为hashMap是没有加锁机制的。因为resize遍历的过程从头到尾将entry放入新的hashMap中,而且是头部插入。没有链表存在还好,一旦有链表存在,极易形成环链。

    比如说同一index下的链表中有a,b,进程1此时e指向a,e.next指向b,进程2此时e指向b,e.next指向a。如此执行下去新的hashMap就会产生环链。

    有人向Sun公司反映,然而人家说了,这本来就不是给多线程使用的,想保证线程安全自己用concurrentHashMap去。



展开阅读全文

没有更多推荐了,返回首页