HashMap
HashMap是开发中常用的集合,我们知道HashMap有几个明显的特点:
- 查找效率高效
- 不是线程安全的
不知道你有没有在注意过HashMap为什么查找效率那么高,为什么不是线程安全的呢?
下面我们就从这两个角度分析HashMap
为什么查找效率高效
数组在查找时通常使用遍历去寻找元素,时间复杂度是O(n)。
而hash在查找时可以理解为有一个索引直接引用到元素所在的位置,就好像数组中的get[i]操作,时间复杂度在某种条件下,经常可以达到O(1),可以说效率非常之高,那么为什么效率这么高呢?
哈希集合类的三个存储概念:
- table:存储所有节点数据的数组
- slot:哈希槽。即table[i]这个位置
- bucket:哈希桶。table[i]上所有元素形成的表或数的集合
先来看看hashmap的数据结构,如图:
HashMap是由数组加链表构成的,table就是数组的长度,所以哈希桶里的元素之和就是HashMap的size。(从jdk1.8开始,可能会随着链表长度的增加进化为红黑树)
明白了哈希结构后我们接着来看哈希结构中的两个重要概念:
- hashcode是根据内存地址所生成的,返回int值,尽可能的离散均匀分布
(关于hashcode的更多应用方式可以参考我的另一篇博客:) - hash(哈希码):hash是与当前集合的table.length进行位运算的结果,以确定hash槽的位置
在hashmap进行查找操作时会使用hashcode对HashMap的table.length(最小为16)进行取模得到hash(哈希码),从而直接定位到某个hash槽中,然后再遍历链表进行查找
温馨提示,在执行add操作时,数据会插入在哈希桶的最前端,刚好是table[i]的位置,便于下次查询,也是提高效率的一种方式。
我们已经找到了HashMap查找效率如此高效的原因,但是缺点也相应而生。
HashMap的比起纯链表结构的集合,结构要更加复杂,那么维护成本就相应的增加了,在不设置初始大小时HashMap的默认容量为16,但是当我们插入第13个元素时,HashMap就会进行扩容,HashMap在扩容时会先开辟一个新的内存空间生成一张新的表,然后根据扩容后的table.length重新计算进行取余,再把计算后的数据放进对应的哈希桶,所以我们在开发中要尽量在创建HashMap对象时要根据自己的业务需要进行初始化集合大小的操作,比如:
HashMap map = new HashMap(50);
非线程安全的主要体现
我们在使用HashMap进行并发编程时经常遇到的问题:
- 数据丢失
- 死链
造成数据丢失的原因主要有以下几个(主要发生在并发场景):
- 在并发赋值时,后面执行的线程有可能把前面执行的线程赋的值覆盖掉
- 在遍历时,对已遍历的区间新增元素会丢失
- 在并发扩容时,后面线程开辟出的内存空间可能会把前面线程开辟的内存空间覆盖掉即使空间内已经有数据
- 在并发扩容时,开辟内存空间后,进行数据迁移时,后面线程的迁移数据可能会把前面线程的迁移数据覆盖掉
- 在扩容时增加数据会丢失
死链:
举个例子,在并发场景增加两个数据5和7,假设他们都被分配到同一个哈希桶中,线程a先执行,先增加了数据5,然后增加了数据7,在链表中7指向了5,就像这样:
然后线程b又做了同样的操作,最终造成环路,如下图:
当然在扩容后,cpu还有一定的运行能力,但get()这时刚好又命中死链所在的solt,上面的场景就是在put造成死链后,get()又命中,使得服务器基本处于宕机状态。
其实HashMap与ConcurrentHashMap的性能相差并不大,但是在线程安全方面确是天差地别,所以我们在并发编程时尽量使用ConcurrentHashMap来替代HashMap。
注:喜欢的小伙伴可以支持一下啊 -^- ,如有建议不胜荣幸!