聊一聊HashMap

HashMap

  • HashMap是Map的实现,默认负载因子0.75,默认初始容量16,每次扩容是原容量的2倍。

  • 因为HashMap没有用到锁,因此是非线程安全的,可以使用HashTable来优化,但是HashTable是锁全表的,因此会影响性能。

  • 在高并发时可以使用ConcurrentHashMap来替代HashMap,在JDK1.8之前,使用的是分段锁,它会对单独的分片进行加锁。在JDK1.8之后,采用的是锁分离,用到的技术是CAS+synchronized,对单独的桶下标上锁,使得锁粒度更细。

两种触发扩容条件

  • 当集合存储容量到达阈值时,会进行扩容

  • 如果Hash冲突比较严重时(桶下面的元素达到8,要变成红黑树了),但是HashMap容量小于64,优先扩容,再考虑树化成红黑树

    • ?为什么 因为冲突已经很严重了,就算他变成红黑树,他指标不治本,优先考虑扩容可以降低hash冲突。

hashmap的基本数据结构图形:

  • 数组+链表+红黑树。

hashmap放入一个数据e的流程,hash+扰动+取余

  • 先计算key的hashcode,然后会对Hash进行高16位和低16位的扰动函数处理

  • 之后再对其和数组长度进行&运算(对于二进制来说&运算会比%运算速度更快),确定桶的位置,之后通过key与桶的元素key值依次进行equals比较如果不相同,则判断是否有链表或红黑树,再通过相应的方法继续比较,如果相同,则对value进行替换。

hashmap中针对于hash的细节改造,为什么高16位还要和低16位进行异或操作(扰动函数处理)?

因为对象的hashcode一般会比较大,如果直接跟hashmap中数组的长度进行取模运算,如果hashmap数组长度太短了hashcode的高位二进制容易无法参与到运算中为了让低16位具有高16位的特征,为了提高随机性,减少哈希冲突,hashmap把hashcode的高16位和低16位进行异或操作。

hashmap的扩容流程,扩容的时机(通过扩容机制引出关键参数,默认容量,负载因子,数组长度,泊松分布)

  • 参数概念:数组长度(hashmap中存数据的数组的长度);负载因子(用于控制什么时候发生扩容 );

  • 当元素数量达到数组长度负载因子时,发生扩容;扩容大小为原数组的2倍,首先创建一个大小为原数组长度2倍的新数组,然后遍历旧数组,将元素逐个的迁移到新数组中;如果桶中是链表结构,会拆分为两个子链表进行迁移。

什么是负载因子,为什么是0.75?(通过阐述过大和过小的优缺点,再提出泊松分布)

0.75是时间和空间上的一个折中考虑,如果是0.5,会导致一般的数组空间被浪费;如果是1的话,在扩容时哈希冲突已经会非常严重了。

扩容后旧数据如何进行分配?要么在原来的位置,要么在nx2的位置,这个结论如何来的?(通过分析二进制得出结论)

什么是hash冲突

两个内容不同的对象经过哈希函数计算后得到的hashcode—样。

hash冲突常见的解决方案(引出拉链法和threadlocal开放地址法)

  • 开放定址法:遇到哈希冲突后,重新找一个新的空闲的哈希地址。

  • 拉链法:参考hashmap,把冲突的元素通过链表的方式组织在一起。

  • 再哈希法:设置多个hash函数,如果第一个冲突了,使用第二个进行计算。

  • 公共溢出区:建立公共溢出区,讲发生哈希冲突的元素都放在公共区域。

hashmap中是如何解决hash冲突的:拉链法解决

hashmap线程安全问题发生的场景和替代解决方案

  • hashmap在put的时候会判断当前桶位置是否已变成链表,是一个if语句,但是这个过程并没有加锁,如果多个线程同时put,并且同时进入if,会导致先put的线程的数据被覆盖掉。

  • hashmap在put时可能会触发扩容,涉及到一个新旧数组的数据迁移过程,如果此时进行get,可能会导致无法get到数据。

为什么要将链表变成红黑树?

链表的时间复杂度是O(n),红黑树的时间复杂度是O(logn),查询速度会更快。

红黑树什么时候退化成链表?

红黑树并不是无限期保持的,当桶中的元素少于6(阈值)时,红黑树就会退化成链表。

在数组扩容的时候,如果一个桶中的红黑树容量太小,分裂之后,如果有一部分容量小于6的话,红黑树会变成链表

具体说一下Hashtable的锁机制(重点问题)

Hashtable 是使用synchronized来实现线程安全的,给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在竞争激烈的多线程场景中性能就会非常差!

HashMap总结:

HashMap是线程不安全的,因为HashMap没有用到锁,多线程使用的时候肯定会出现数据不一致的问题。所以我们要对它加锁,所以使用HashTable,虽然HashTable线程安全,但它锁的是全表,效率比较低。之后用到了ConcurrentHashMap,在]DK1.8之前,使用的是分段锁,它会对单独的分片进行加锁。在JDK1.8之后,采用的是锁分离,用到的技术是CAS+synchronized,对单独的桶下标上锁,使得锁粒度更细。

说白了,HashMap的演进其实就是线程安全问题的解决,锁粒度的细分,HashMap没有锁,hashTable锁全表,ConcurrentHashMap根据不同的版本分为JDK1.8之前和之后,分别锁的是segment分段和桶下标。

ConcurrentHashMap,jdk1.8以前(1.7)用的是ReentrantLock,segment默认为16,其中,用volatile修饰了HashEntry 的数据 value和下一个节点next,保证了多线程环境下数据获取时的可见性!

小结

  • 扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容;

  • 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊;

  • HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap;

  • JDK1.8引入红黑树大程度优化了HashMap的性能。

  • 21
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值