HashMap
-
hashmap 特点
非线程安全的,无序的,基于 map 接口,允许 null 值(把 null 当成普通的值来存,不允许有 null key 重复),底层是 1.7 数组+链表,默认长度为 16,负载因子为:0.75。 如果链表长度大于 8 的话,就变链表为红黑树
-
线程安全的实现方式
-
Collections.synchronizedMap(new HashMap)
-
ConcurrentHashMap
-
-
hash 碰撞
碰撞的意思是计算得到的Hash值相同,需要放到同一个bucket中 Hashmap里面的bucket出现了单链表的形式,散列表要解决的一个问题就是散列值的冲突问题,通常是两种方法:链表法和开放地址法。
链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位; 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。
-
负载因子和初始容量
负载因子:比如说当前的容器容量是16,负载因子是
0.75
,``16*0.75=12`,也就是说,当容量达到了12的时候就会进行扩容操作。当负载因子是1.0的时候,也就意味着,只有当数组的8个值(这个图表示了8个)全部填充了,才会发生扩容。这就带来了很大的问题,因为Hash冲突时避免不了的。当负载因子是1.0的时候,意味着会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率。
-
负载因子什么是 0.75?(时间和空间的权衡)
源码注释: 负载因子是0.75的时候,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率。
-
拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?
之所以选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,链表长度低于6,就把红黑树转回链表,因为根本不需要引入红黑树,引入反而会慢。
-
什么时候扩容?
当数量大于 默认容量*负载因子的时候就会进行扩容:会进行 rehash+复制数据,十分的消耗性能
-
1.7 put 方法
- 判断当前数组是否需要初始化。
- 如果 key 为空,则 put 一个空值进去。
- 根据 key 计算出 hashcode。
- 根据计算出的 hashcode 定位出所在桶。
- 如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值。
- 如果桶是空的,说明当前位置没有数据存入;新增一个 Entry 对象写入当前位置。
-
1.7 get 方法
- 首先也是根据 key 计算出 hashcode,然后定位到具体的桶中。
- 判断该位置是否为链表。
- 不是链表就根据
key、key 的 hashcode
是否相等来返回值。 - 为链表则需要遍历直到 key 及 hashcode 相等时候就返回值。
- 啥都没取到就直接返回 null
-
1.8 put
- 判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始化)。
- 根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。
- 如果当前桶有值( Hash 冲突),那么就要比较当前桶中的
key、key 的 hashcode
与写入的 key 是否相等,相等就赋值给e
,在第 8 步的时候会统一进行赋值及返回。 - 如果当前桶为红黑树,那就要按照红黑树的方式写入数据。
- 如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)。
- 接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。
- 如果在遍历过程中找到 key 相同时直接退出遍历。
- 如果
e != null
就相当于存在相同的 key,那就需要将值覆盖。 - 最后判断是否需要进行扩容。
-
1.8 get
- 首先将 key hash 之后取得所定位的桶。
- 如果桶为空则直接返回 null 。
- 否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。
- 如果第一个不匹配,则判断它的下一个是红黑树还是链表。
- 红黑树就按照树的查找方式返回值。
- 不然就按照链表的方式遍历匹配返回值。
-
为什么要转成红黑树
当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为
O(N)
。 -
map 遍历方法
- entrySet 同时获取 key 和 value
- 使用 keyset
- 使用 java8 的foreach 传入 BitConsumer
-
hashmap 是怎么 rehash 的?和 redis rehash 的方法有什么不同?
在扩容的时候进行 rehash,当map中的元素数量达到阈值时,会重新创建一个新的数组,长度为旧数组的两倍(如果长度没有达到上限的话),这时会依次对旧数组(包括其中的链表)按顺序重新计算索引插入,之后重新赋值引用即可
redis 的 rehash 是渐进式的,redis 会在rehash的同时,保留新旧两个hash结构,查询时会同时查询两个hash结构,然后在后续的定时任务以及hash操作指令中,循环渐进地将旧hash的内容一点点地迁到新的hash结构中。当搬迁完成了,就会使用新的hash结构取而代之。当hash移除最后一个元素后,该数据结构自动删除,内存被回收。
-
hashmap 为啥不是线程安全的?TODO
并发下的 rehash 是不安全的 容易造成 Infinite Loop。
-
为什么使用红黑树代替链表?
遍历查询链表的效率太低了。O(N) 红黑树O(logN)
ConcurrentHashMap
-
为什么是线程安全的?
1.7 前是数组加链表。 ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
1.8 后是抛弃了 segment 分段锁,使用 CAS+ synchronized来保证并发安全性。
为什么 1.8 进行了优化? 一是 1.7 并发较少,1.8 数据结果上也进行了调整(链表查询的速度慢)。