HashMap是java中十分罕用的容器,以前只是停留在应用阶段,对于它的底层设计更是只知其一;不知其二,看到源码才晓得它奇妙的设计和工作原理。在理解底层原理之前,倡议先学习相干的数据结构几种常见数据结构,能够帮忙你更好的理解HashMap。

数据结构

HashMap的数据结构是数组+链表,默认初始大小是16,默认负载因子是0.75,当理论容量大小(容量大小是蕴含每一个node的大小,不是非空数组的大小)超过定义的容量Capacity乘以负载因子的值后,HashMap会进行一次resize扩容操作,创立一个2倍大小的新数组,同时rehash所有的数据(之所以须要rehash,而不是间接复制之前的数组地位,是因为hash算法中须要应用到数组长度,扩容后数组的长度变动了)

HashMap初始大小是16,设置成2的幂,便于进行位运算,另外,在手动设置HashMap大小的时候,也尽量设置成2的幂数

链表进化和进化机制:hashmap中的链表长度在大于8的时候会进化成红黑树结构,小于6的时候又会进化成链表。那么,为什么是8和6呢?那是因为在屡次的试验中发现,产生8次hash碰撞的概率十分十分小,设置成8能够在小概率事件产生后,转换数据结构,优化后续查问性能,进化阈值设置成6是为了防止设置成8或者7的时候,因为hash碰撞在8左近来回的切换数据结构.

Hash算法

每一个key会通过一次hash运算(先获取key的hashcode,将hashcode无符号右移16位[hashcode >>> 16],对右移后的值和hashcode进行异或运算(二进制里雷同得0,不同得1))[hash = hashcode ^ hashcode >>> 16],失去key的hash值,至于数组的index值,须要将hash值和数组长度减一后的值进行与运算(同时为1后果为1,否则为0)[(length - 1) & hash]失去。

hashmap产生hash碰撞的数组会应用尾插法(jdk7是头插法)插入一个node,node中蕴含key,hash值,value以及下一个node的指针next

tips:<<左移,左边补0 >>右移,右边初始位为正补0,为负补1 >>>无符号右移,初始位补0 对于二进制来说,初始位0为正,1为负

线程平安

HashMap的线程不平安,jdk7和jdk8的起因不同

在jdk7中,应用的是头插法,会有两种状况的线程不平安问题
(1)在某一个数组的索引的地位,A线程要插入一对key-value,指针next指向第一个Entry,这个时候B线程也要插入一对key-value,因为A线程的赋值还未实现,所以Entry的next也指向第一个Entry,并胜利增加。随后线程A开始进行赋值操作,这个时候就会发现,在线程A执行完之后,线程B所增加的数据被笼罩了
(2)有一个链表有两个相邻的Entry,Entry1和Entry2,Entry1的next指针指向Entry2,当线程A在执行resize操作的时候,Entry1还没有胜利执行完搁置到新数组并更新next的操作的时候,切换到B线程,B线程操作实现了整个HashMap的resize,此时扩容后的HashMap,因为头插法的存在,Entry2的next指针指向Entry1,同时切换到线程A,线程A继续执行Entry1搁置到新数组中的操作,Entry1的next指针指向Entry2,又因为Entry2的next指针是指向Entry1的,所以会陷入死循环,线程A无奈跳出resize这步

在jdk8中,因为尾插法的存在,不会呈现死循环的状况,然而会呈现数据笼罩的问题。假如线程A和线程B同时在put 值,它们的key的hash值通过计算后失去的index索引值雷同,此时线程A挂起,线程B进行了更新操作,之后线程A持续进行更新操作,会笼罩后面线程B的更新,线程B put的数据就隐没了