HashMap相关知识点

一. 谈谈你理解的 HashMap,讲讲其中的 get put 过程。

1.7版本:(数组+链表)
put():

  • 判断当前数组是否需要初始化。
  • 如果 key 为空,则 put该值进去。
  • 根据 key 计算出 hashcode。
  • 根据计算出的hashcode 定位出所在桶。
  • 如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key相等,如果相等则进行覆盖,并返回原来的值。
  • 如果桶是空的,说明当前位置没有数据存入,就新增一个 Entry对象写入当前位置;或遍历完链表后,将Entry对象插入到链表的首部。(add函数需要判断是否需要扩容。扩容的过程需要rehash,transfor相对费时,所以最好预先估计设置Hashmap的容量,避免扩容带来的性能损耗)

get():

  • 首先也是根据 key 计算出 hashcode,然后定位到具体的桶中。
  • 判断该位置是否为链表。
  • 不是链表就根据 key、key 的 hashcode 是否相等来返回值。
  • 为链表则需要遍历直到 key 及 hashcode 相等时候就返回值。(equals比较)
  • 啥都没取到就直接返回 null

1.8版本:(数组+链表+红黑树)
当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。1.8 中重点优化了这个查询效率。修改为红黑树之后查询效率直接提高到了 O(logn)。
put(),get()操作跟1.7版本大致相同,中间插入了这样的判断
put():

  • 如果当前桶为红黑树,那就要按照红黑树的方式写入数据。
  • 如果是个链表,就需要将当前的 key、value封装成一个新节点写入到当前桶的后面(形成链表)。
  • 接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。
    get():
  • 如果第一个不匹配,则判断它的下一个是红黑树还是链表。
  • 红黑树就按照树的查找方式返回值。
  • 不然就按照链表的方式遍历匹配返回值。

二.1.8 做了什么改动?

1.加入红黑树优化了查询
2.扩容机制
jdk1.7扩容:
默认初始容量16,默认负载因子大小是0.75,当一个map填满了75%的bucket时,和其他集合类一样,就会创建原来HashMap大小两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中,这个过程叫rehashing,因为它调用了hash方法找到新的bucket的位置。
jdk1.8扩容:
在计算新位置的时候并没有跟1.7中一样重新进行hash运算,而是用了原位置+原数组长度这样一种很巧妙的方式,而这个结果与hash运算得到的结果是一致的,只是会更快。rehash之后,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
3.头插入改为尾插入
Jdk8是元素在加在链表的尾部,原因是他必须遍历链表,要知道阈值的大小,变化红黑树,还能解决JDK1.7的死循环的情况,一举两得。

三.是线程安全的嘛?

不是。

四.不安全会导致哪些问题?

并发场景下使用时容易出现死循环。https://coolshell.cn/articles/9606.html
两个线程同时往HashMap里放Entry,同时,HashMap正好需要扩容,如果一个线程已经完成了transfer过程,而另一个线程并不知道,并且又要transfer的时候,死锁就会形成。(transfer过程是将旧数组中的元素复制到新数组中) 就是这里的并发操作容易在一个桶上形成环形链表

五. 如何解决?有没有线程安全的并发容器?

HashMap:无论是 1.7 还是 1.8 其实都能看出 JDK 没有对它做任何的同步操作,所以并发会出问题,甚至出现死循环导致系统不可用。因此 JDK 推出了专项专用的 ConcurrentHashMap ,该类位于 java.util.concurrent 包下,专门用于解决并发问题。

六.ConcurrentHashMap 是如何实现的? 1.7、1.8 实现有何不同?为什么这么做?

1.7版本:
在ConcurrentHashMap中,定义了一个Segment<K, V>[]数组来将Hash表实现分段存储,从而实现分段加锁;而么一个Segment元素则与HashMap结构类似,其包含了一个HashEntry数组,用来存储Key/Value对。Segment继承了ReetrantLock,表示Segment是一个可重入锁,因此ConcurrentHashMap通过可重入锁对每个分段进行加锁。
每个Segment元素和 HashMap 非常类似,唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,volatile类型的变量可以保证其在多线程之间的可见性,因此可以被多个线程同时读,从而不用加锁。
get操作:只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再定位到HashEntry,再遍历链表获取对应值。由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。
put操作:首先是通过 key 定位到 Segment,之后在对应的 Segment 中进行具体的 put。(虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。)首先需要调用tryLock()方法获取锁,然后通过hash算法定位到对应的HashEntry,然后遍历整个链表,如果查到key值,则直接插入元素即可;而如果没有查询到对应的key,则需要调用rehash()方法对Segment中保存的table进行扩容,扩容为原来的2倍,并在扩容之后插入对应的元素。插入一个key/value对后,需要将统计Segment中元素个数的count属性加1。最后,插入成功之后,需要使用unLock()释放锁。
1.8 版本:
抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

七.和HashTable的区别?

HashMap:线程不安全,非synchronized.速度快。可以接受null的key和value。
HashTable:线程安全,synchronized修饰方法.速度慢。不可接受null的key,value。

八.HashSet

HashSet是基于HashMap来实现的。Map有键和值,HashSet相当于只有键,值都是相同的固定值,即PRESENT。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值