Hashmap原理:是由一个entry数组组成的,每个entry实体本质上是一个键值对再加上一个next指针。在往数组中添加元素的时候,会首先通过hash运算来确定数组的位置,然后因为hash冲突的存在,每个数组元素的next指针后边又挂一个entry实体,就形成了一个链表结构。当链表长度大于8时,就会将链表转成红黑树。
解决hash冲突的办法:开放定址法,链地址法,再hash法,公共溢出区域法,然后hashmap采用的是链地址法。
可以用LinkedList代替数组结构吗:首先是可以的,但是我们通过hash运算已经确定了元素在数组中的位置,数组效率要比LinkedList高。其次若使用ArrayList替代数组,就没办法灵活定义hashmap的两倍扩容机制,因为ArrayList是1.5倍扩容。
Hashmap在什么条件下扩容:为了在一定情况下避免hash冲突,在hashmap中的键值对数量大于负载因子*数组长度的时候,会将数组的长度扩大为原来的两倍,相应的,负载因子*数组长度也会扩大两倍。
为什么扩容是2的次幂:计算元素对应位置的代码是(table.length-1)&hash, 2的次幂对应的二进制结尾是0000,减一之后结尾就是1111,与别的hash值与运算之后得到的结果不一样的几率高,能够有效的避免hash冲突。
Hashmap中put元素的过程:对key的hashcode做hash运算,确定在数组中的位置。若没出现hash冲突,就直接放在数组的对应位置,若出现了冲突,就挂在相应位置元素的next指针后边,形成一个链表。在jdk1.8中,链表长度大于8的时候,会将链表转成红黑树存储以增加查询效率。在hashmap键值对数量大于负载因子*数组长度的时候,会将数组长度扩大至原来的两倍来完成扩容。
Hashmap中get元素的过程:会首先对key的hash值做hash运算,如果对应的数组位置直接命中,就返回,若没有,就在对应的链表或红黑树里边采用一个个比较的方式去get。
Hash算法的作用:hash函数是指把一个大范围映射到一个小范围,以减少存储空间,常见的hash算法有MD4、MD5。
String的hashcode实现:以31为权,与每一位字符的ascII值进行计算,结果赋值给int,以自然溢出取模。
Jdk1.8中的hashmap与之前有哪些不同:1>有数组+链表变成数组+链表+红黑树 2>优化了高位运算的hash算法 3>扩容后,元素要么是在原位置,要么是在原位置再移动2次幂位置,且链表顺序不变(因为最后一条的变动,hashmap在1.8中,不会出现死循环的问题)。
为什么会有链表转红黑树、红黑树转链表的操作:在链表长度大于8的时候,转成红黑树,主要是为了查找方便(时间复杂度为O(logn))。红黑树在添加元素的时候,涉及到左旋、右旋、变色这些操作,因此在元素个数较少时,只使用了链表这种数据结构。同时,在红黑树的情况下,若元素个数小于6,就会将红黑树转成链表存储。6和8的区别主要是为了减小因频繁对一个hashmap做添加,删除(两种数据结构转化)而带来的性能开销。
为什么不用二叉查找树代替红黑树:二叉查找树在特殊情况下又是一条线性结构。
一般什么适合做hashmap的key:首先null可以作为hashmap的key,且entry对象会放在数组的第一个位置;一般用不可变对象hashmap的key,例如string,long,integer;如果用可变对象作为hashmap的key值,必须重写equals和hashcode的方法,同时若hashcode若发生变化,会导致get不到的问题发生。
Hashmap的并发问题:1.7hashmap在链表情况下put元素时,由于采用的是头插法,在扩容之后链表的顺序与原来刚好相反,在并发情况下,做循环扩容,可能导致两个节点成环,下一次去get到对应的桶,会死循环。1.8在链表情况下改成了尾插法,避免了这个问题。至于1.7为什么采用头插法,可能是考虑最近插入的数据是热点数据,放在第一位,方便查找。
https://blog.csdn.net/yxh13521338301/article/details/105629318
(((假设一开始是A->B->C-D,第一个线程获取了A,获取了下个节点A.next是B做下次循环,此时线程被挂起,第二线程哒哒跑完了整个流程,此时链表变成了D->C->B->A(假设扩容后都在同个桶里),接着第一个线程继续跑,A跑完,A->NULL,下一个之前获取是B(本来这时候A下一步应该是Null,但是链表已经被上一个线程修改了,但是B是改之前获取);B跑完,此时变成B->A->NULL,b.next就是A了(重点);A继续循环,此时变成A->B->A->null,A.next=null跳出循环结束。后面get()获取到该桶的时候就一只循环呀循环,就导致了死循环了)))
然后并不是说1.8就是线程安全了,中在put过程结束之后,对hashmap计数的++size()操作不是原子操作,即在多线程情况下计数可能会有问题。另外两个线程同时对相同位置的桶做添加数据的操作,可能会有一个线程添加失败又或者只会成功添加一个线程里的值。