什么是Hashmap?
HashMap是一个用于存储Key-Value键值对的集合(一对多),是基于哈希表的一个Map接口实现
底层原理:
对于HashMap,我们最常使用的是两个方法:get() 和 ****put()。
Hashmap的默认初始长度?
Hashmap的默认初始长度是16,并且每次自动扩展或是手动初始化时,长度必须是2的幂。
之所以选择16,是为了服务于从Key映射到index的Hash算法
从Key映射到HashMap数组的对应位置,会用到一个Hash函数:
index = Hash(“ ”)
为了实现高效的Hash算法,Hashmap的发明者采用了位运算的方式,公式如下(Length是HashMap的长度):
index = HashCode(*Key*) & (*Length* - 1)
如果不是2的幂次方,散列结果将会大大下降。导致出现大量链表(查询速度变慢)
负载因子为什么是0.75?
负载因子 = 元素个数 / 内部数组总大小
loadFactor = threshold / caacity
Hashmap底层的数据结构:
我们的数据一开始是保存在数组里面的,当发生了Hash碰撞的时候,就是在这个数据节点上,生出一个链表,当链表长度达到一定长度的时候,就会把链表转化为红黑树。
加载因子是表示Hash表中元素的填满的程度。
加载因子越大,填满的元素越多,空间利用率越高,但冲突的机会加大了。
反之,加载因子越小,填满的元素越少,冲突的机会减小,但空间浪费多了。
冲突的机会越大,则查找的成本越高。反之,查找的成本越小。
因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷。
那为什么不是1或是0.5呢?
如果是0.5 , 那么每次达到容量的一半就进行扩容,默认容量是16, 达到8就扩容成32,达到16就扩容成64, 最终使用空间和未使用空间的差值会逐渐增加,空间利用率低下。 如果是1,那意味着每次空间使用完毕才扩容,在一定程度上会增加put时候的时间。至于为什么是0.75?查了下1.8:
Ideally, under random hashCodes, the frequency of nodes in bins follows a Poisson distribution with a parameter of about 0.5 on average for the default resizing threshold of 0.75, although with a large variance because of resizing granularity. Ignoring variance, the expected occurrences of list size k are (exp(-0.5) pow(0.5, k) / factorial(k)). The first values are:
0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
翻译过来就是说:
理想情况下,在随机哈希码下,存储箱中节点的频率遵循泊松分布,默认大小调整阈值为0.75时,平均参数约为0.5,但由于大小调整的粒度,变化较大。忽略方差,列表大小k的预期出现次数是(exp(-0.5)pow(0.5,k)/factorial(k))。
HashMap为什么是线程不安全的?
1.put的时候导致的多线程数据不一致
比如有两个线程A和B ,先A希望插入-个key-value对到HashMap中,首先计算记录所要落到的hash桶的索弓|坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A-样执行,不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的hash桶索弓|和线程B要插入的记录计算出来的hash桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此-来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了, 造成了数据不一致的行为。
- resize而引|起死循环
这种情况发生在HashMap自动扩容时,当2个线程同时检测到元素 个数超过数组大小x负载因子。此时2个线程会在put()方法中调用了resize() ,两个线程同时修改一个 链表结构会产生一个循环链表( JDK1.7中,会出现resize前后元素顺序倒置的情况)。接下来再想通过get()获取某一个元素,就会出现死循环。
1.7和1.8的HashMap的不同点
HashMap在JDK1.8之前的实现方式 数组+链表,但是在JDK1.8后对HashMap进行了底层优化,改为了由 数组+链表+红黑树实现,主要的目的是提高查找效率。
hash冲突
当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式
链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。