JDK8-HashMap源码分析

HashMap vs HashTablehtml

         HashTable若是插入key/value为null的值时,会报错,可是hashmap不会,在hashmap中,null是做为第0个元素的,至关因而作了特殊化处理。算法

         前者是非线程安全的,后者是线程安全的. 后者线程安全的缘由就是由于后者的每个方法上都有一个synchronized,这样虽然保障了线程安全,可是每次都要锁整个class对象,而且还会阻止其余synchronize方法的访问,因此效率低下!  HashTable已经废弃了,尽可能别使用了.shell

         因此综上所述,HashTable效率低的缘由是由于全部访问它的线程都必须竞争同一把锁,若是容器里面不一样的数据有多把锁,那么执行的效率就高了,因此ConcurrentHashMap使用的就是锁分段的技术.数据库

 

JDK1.8以后,HashMap和以前1.7不一样的是,再也不单纯的由数组+链表的方式实现的(所谓的链地址法).  1.7因为在Hash冲突的时候,在桶上造成的链表会愈来愈长,这样在查询的时候效率就会变低,1.8以后改为了由红黑树实现.即:数组

 

 

因此,JAVA8中的HashMap是由: 数组+链表/红黑树组成安全

 

         Java7使用Entry来表示每一个HashMap中的数据节点,8中使用了Node,基本没什么区别,都是key,value,hash和next这四个属性来修饰链表,而红黑树的状况须要使用TreeNode函数

 

类属性分析:

默认容量为: 16性能

 

 

         这里的默认容量为何是2的n次幂?.net

         查看它的put方法可知,key在Node[]中的下标为: (n - 1) & hash。若是这个n是2的N次幂,那么hash至关于和111***111作与运算,数据分散的就比较均匀。若是n不是在的N次幂,即hash有可能和1110作与运算,那么最后一位怎么与都是0,那至关于结尾是1的那几个下标永远都不会放数据了,好比0001,0011…这确定会增长碰撞的概率.线程

         若是size > capacity*loadFactor的话,hashmap还会进行resize操做,会至关耗性能!因此若是事先能够肯定你要放进hashMap中的数据大小。那么应该尽可能设置成loadFactor * 2^n >  initCapacity ,这样既考虑了&的问题,也避免了resize的问题.

         可是后面提到会有tableSizeFor()和在put的时候考虑loadFact来保证上面这两个要求的.因此在初始化的时候,设置成本身知道的大小便可,冲突这些由hashmap自身来帮你减免!

         不对,仍是得本身算下,若是你输入的initcapacity为7,那么算出来的最近的2^n为8,若是选择的是默认的0.75,即最多放入6个元素就要扩容,你放到第7个的时候,仍是要resize()…

 

最大的capacity为 2^30

 

 

 

默认负载因子为0,75,即size > capacity * 0.75,hashmap就要进行扩容

 

 

特殊状况下:

1)内存空间不少,时间效率要求很高,能够下降loadFactor(尽可能让table扩宽)

2)若是内存紧张,时间效率要求不高,能够增长loadFactor

 

一个桶中,bin(箱子)的存储方式由链表转成红黑树的阈值为8

 

 

一个桶中,由红黑树转成链表的阈值,resize的时候可能会用到这个值.

 

 

 

当桶中的bin被树化时最小的hash表容量.若是树化时bin的数量太多会进行resize扩容.注释中说MIN_TREEIFY_CAPACITY至少是 4 * TREEIFY_THRESHOLD

 

 

 

上面说了这么多的bin,这里该介绍下bin究竟是个什么结构了.

每一个bin在HashMap表明存储了一个K/V键值对,结构定义以下:

 

 

 

 

 

Hashmap中计算key的hash值

 

 

能够看到它并无直接使用Object中生成hashcode的方法,这个方法叫扰动函数,和以前的要将capacity设计成2^n同样,也是为了减小碰撞用的.

根据前面可知,key在Node[]中的下标为 key.hashCode & (n-1),咱们知道key.hashCode是一个很长的int类型的数字(范围大概40亿),而n-1显然没有这么长,若是直相与,那么只有key.hashCode的后面几位参与运算了,显然会使得碰撞很激烈!加了这个函数以后,让高位也想办法参与到运算中来,这样就有可能进一步下降碰撞的可能性了!

 

用于存储Node(K/V)对的hash表(数组),为何是transient?

 

 

为了解答这个问题,咱们须要明确下面事实:

Object.hashCode方法对于一个类的两个实例返回的是不一样的哈希值

能够试想下面的场景:

咱们在机器A上算出对象A的哈希值与索引,而后把它插入到HashMap中,而后把该HashMap序列化后,在机器B上从新算对象的哈希值与索引,这与机器A上算出的是不同的,因此咱们在机器B上get对象A时,会获得错误的结果。

因此说,当序列化一个HashMap对象时,保存Entry的table是不须要序列化进来的,由于它在另外一台机器上是错误的,因此属性这里为transient。

由于这个缘由,HashMap重写了writeObject与readObject 方法

 

保存K/V对的Set

 

 

目前hashMap中K/V对的数量

 

 

 

每次对这个hashmap作操做,这个modCount就会改变.(CAS?!)

 

 

 

Threadhold表示当容量达到该值时,会进行resize

loadFactor表示用户设置的负载因子大小

 

 

 

 

构造函数:

         有三种,一种是无参的,这个没啥好看的,一种是只配置了initialCapacity,最后一种是设置了initcapacity和loadFactor。看第三种就够了,第二种不过是将loadFactor这个形参用默认0.75传入而已.

 

 

方法内部是将loadFactor这个属性设置为用户输入的大小,有意思的是tableSizeFor(initCap)这个函数,也就是说你输入一个10,hash表的大小不必定就是10. 这个函数的功能就是用来保证容量应该大于cap,且为2的整数幂.

 

随便写个数带进去算一下就可知道,该算法的做用让最高位的1后面的位全变为1.而后再+1,获得的就是恰巧的2的n次幂.

 

 

         注意table的初始化是在第一次put的时候作的,那个时候还有考虑loadFactor再作一次tableSize的计算,那个时候获得的就是最合适的那个2的n次幂的那个数了!厉害啊!

 

 

因此,看下put流程吧!这个很重要:

方法流程以下:

 

 

方法定义以下:

 

 

False表示,会改变existing value,至关因而key相同的话会作替换.

True表示,table不在creation mode.(这是啥意思?!)

 

第一次或者扩容的时候会调用resize():

  1. 若是是第一次初始化且没有输入initcapacity

那么newCap=16,  threshold = 0.75 * 16

         Hash表大小为: table = Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]

  1. 若是是第一次初始化,可是设置了initcapacity和loadFactor

注意,此时的threadhold(=initcapacity接近的2^n那个值) 和 loadFactor已经有值了,可是table==null,由于没有初始化嘛,因此此时oldCap=0

  • oldThreadHold = threadhold(=initcapacity接近的2^n那个值)

此时 newCap = threadhold

 threshold =newThreadHold = newCap * loadFactor (threadhold还变小了!…)

Hash表大小为: table = Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]

  1. 若是不是第一次初始化

此时 oldTab !=null  oldCap = oldTable.length  oldThreadHold = threadhold

那么 newCap = oldCap << 1 为原来的一倍 newThreadholder也为oldThr的一倍,即都扩大为原先的一倍!

         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

         还要将原先旧Table里的数据转移到新的table中.

  1. 插入完成后,还要检查下是否须要扩容

即 ++size > threshold  那么须要进行resize

 

         这里让我想到有问题的可能就是这个了,首次初始化,cap为16,输入的initcapacity为15的时候,会涉及到一次扩容(若是没有冲突的话—因此这里的扩容一倍啥的操做应该是取了个折中!否则为了避免冲突再扩容一倍很耗费空间啊!并且再扩容一倍也不能保证不冲突)

 

resize()流程分析:

(和Java7不一样,8中resize后元素顺序是不变的)

         归纳起来就是:

  1. 原先有值的状况下

若是已是MAXIMUM_CAPACITY,那么返回原先数组,不然将容量扩大为原来的一倍,即newCap = oldCap << 1, newThr=oldThr << 1

  1. 原先没有值,可是构造函数指定了initCapacity

newCap = oldThr, newThr随后会被指定为newCap*loadFactor

  1. 原先没有值,且没有指定initCapacity,即无参构造函数

按照默认的值初始化,即initCapacity=16, newThr=0.75*16

  1. 而后使用newCap构建一个newTab,若是旧表不为空就要迁移数据

迁移数据的流程以下:

 

 

  1. 若是只有一个元素时,那就从新计算位置,插入新的table
  2. 若是节点是树类型,那使用树的插入方式(这个暂时还不太了解)
  3. 若是节点是链表类型,由于元素放的位置取决于tab[i = (capacity - 1) & hash]。当长度扩为原来的2倍时,由于oldCap和newCap是2的次幂,而且newCap是oldCap的两倍,就至关于oldCap的惟一一个二进制的1向高位移动了一位,结论为元素要么在原先位置,要么在原位置上再移动2次幂。

举例:

好比原来容量是16,那么就至关于index=e.hash & 0x1111

如今容量扩大了一倍,就是32,那么index=e.hash & 0x11111

如今(e.hash & oldCap) == 0就代表:

已知: e.hash & 0x1111 = index

而且: e.hash & 0x10000 = 0

那么: e.hash & 0x11111 不也就是原先index的值!

 

get()流程分析:

         若是获取到的Node为null则返回null,不然返回Node中存储的值。

 

 

         点到getNode()中继续查看

 

 

         分四步进行:

                  首先,若是table未空,直接返回null。不然就查找节点

                  再者,计算hash获得的位置刚符合,那直接返回。(只有一个bin的状况)

                  若是是红黑树,按红黑树的方式查找

                  若是是链表,逐个查找。 找不到返回null。

 

 

entrySet分析:

         遍历的话,使用此种方式。比每次从Map中从新获取一个key要快多了!

不是每次都是new EntrySet(),可是暂时没找到这个东西在哪里填充的。

 

 

         网上的说法是遍历的原理就是hashmap实现的原理。entrySet()该方法返回的是map包含的映射集合视图,视图的概念至关于数据库中视图。提供一个窗口,没有具体到相关数据,而真正获取数据仍是从table[]中来。(ps:能够借鉴下hashmap的foreach方法,即先遍历完数组中的第一个链表,再遍历数组中的下一个链表…)

 

 

 

 

HashMap为何线程不安全:

         Java8之前线程不安全是在于在resize()的时候会在get的时候产生死循环,而之因此产生死循环是由于resize以后,元素的前后顺序会相反。

         即转移的时候是这样的:每次取出旧数组的头结点的next,以后从新计算头结点在新的Hash中的位置,而后将头节点的next指向新的table[i],而后把table[i]设置成当前的头结点,那么就完成了头结点的转移。

   

 

         这时候,线程一种3.next是7,线程二中7.next是3,e.next = newTable[i]就会造成了环形链表,因此在get的时候就会一直循环在这里。

而且在迭代的过程当中,若是有线程修改了map,会抛出ConcurrentModificationException错误,就是所谓的fail-fast策略。

 

         Java8以后,由于顺序是相同的,因此上面的那个环形链表问题就没有了。可是后面那个问题仍是有的,因此仍是线程不安全的。另外还有++size的操做也不是线程安全的!

 

 

 

 

 

 

 

参考:

http://www.iteye.com/topic/539465 (initailCapacity为何要设置成2的n次幂?)

https://blog.csdn.net/dog250/article/details/46665743#comments (红黑树的一种解释)

https://coolshell.cn/articles/9606.html(Java7中为何会造成环形链表)

相关文章
相关标签/搜索