深入理解hashMap

目录
1. HashMap是如何实现原理?
2. HashMap采用的hash算法是什么?
3. 为什么map进行2倍扩容?
4. HashMap 的扩容机制?
5. 为什么要引入红黑树?
6. 红黑树专题
7. 多线程下HashMap 出现的问题
8. HashTable与HashMap的区别?
9. ConcurrentHashMap 的原理?
10. ConcurrentHashMap 存在的问题?
11. 同步容器和并发容器

1. HashMap是如何实现原理?

是一个散列表,它存储的内容是键值对(key-value)映射。继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。它的实现不是同步的,这意味着它不是线程安全的。数据结构为:数组+链表+红黑树。数组是HashMap的主体,链表和红黑树则是主要为了解决哈希冲突而存在的。
Put操作—key为null的存入散列桶0中,key不为null的,根据key的hashcode并对hashcode进行均匀散列,得到一个hash值,相当于数据的索引,然后开始顺序遍历这个链表,如果有等于key值的Entry,就更新Entry的值,并返回旧值;否则遍历完链表之后,进行新Entry的添加,添加之前需要判断容器的size是否已经大于等于threshold(装载因子*数组长度),如果是,进行扩容(一般为2*table.length,当table大小等于2^30时,就不进行扩容了。默认加载因子是 0.75),否则,new一个Entry并采用头插法添加到hash值对应的散列桶中。
Get操作—key为null,从散列桶0中,查找Entry的key为null进行返回,不存在的话,返回null;key不为null,用同样的hash函数获得hash值,遍历hash值对应的散列桶,查找该链表中是否存在相应的key,如果存在就返回对应的value,否则返回null
equals和hashcode—两个对象equals之后返回true,应该要返回相同的hashcode;如果重写了equals,但是没有重写hashcode,会导致插入的两个对象实际上是相同的,但是还是能够插入成功。

2. HashMap采用的hash算法是什么?

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
而每次放入散列桶中的hash(key) & (length-1)位置。(h = key.hashCode()) ^ (h >>> 16)的高位和低位亦或使得hashcode值尽可能的分散,减小碰撞的几率。

3. 为什么map进行2倍扩容?

这个方法非常巧妙,它通过h & (table.length -1) 来得到该对象的保存位,保证获取的index一定在数组范围内。而HashMap 底层数组的长度总是2 的n 次方,2n-1 得到的二进制数的每个位上的值都为1,那么与全部为1 的一个数进行与操作,速度会大大提升。当length 总是2 的n 次方时,h& (length-1)运算等价于对length 取模,也就是h%length,但是&比%具有更高的效率。还有扩容后结点的新位置可以计算出来。

4. HashMap 的扩容机制

当HashMap 中的结点个数超过数组大小*loadFactor(加载因子)时,就会进行数组扩容resize(),loadFactor 的默认值为0.75。也就是说,默认情况下,数组大小为16,那么当HashMap中结点个数超过16*0.75=12 的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,并放进去,而这是一个非常消耗性能的操作。
因为使用的是2次幂的扩展,假设bucket大小n=2^k,元素在重新计算hash之后,因为n变为2倍,那么新的位置就是(2^(k+1)-1)&hash。而2^(k+1)-1=2^k+2^k-1,相当于2^k-1的mask范围在高位多1bit(红色)(再次提醒,原来的长度n也是2的次幂),这1bit非1即0。所以,我们在resize的时候,不需要重新定位,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话位置没变,是1的话位置变成“原位置+oldCap”。如图:

5. 为什么要引入红黑树?

JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。
当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。
针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。而且红黑树查找性能稳定。
为什么要多于8个结点才用红黑树呢?
因为红黑树需要进行左旋,右旋操作,增删复杂。而单链表简单。如果元素小于8个,链表就足够好了。随着结点增多,链表查询和修改删除成本增高。因此用红黑树优化。

6. 红黑树专题

1) 什么是红黑树?
红黑树本质上是一种二叉查找树,但它在二叉查找树的基础上额外添加了一个标记(颜色),同时具有一定的规则。这些规则使红黑树保证了一种平衡,插入、删除、查找的最坏时间复杂度都为 O(logn)。
它的统计性能要好于平衡二叉树(AVL树),因此,红黑树在很多地方都有应用。比如在 Java 集合框架中,很多部分(HashMap, TreeMap, TreeSet 等)都有红黑树的应用,这些集合均提供了很好的性能。
在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
 性质1. 节点是红色或黑色。
 性质2. 根节点是黑色。
 性质3 每个叶节点(NIL节点,空节点)是黑色的。
 性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
 性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。

2) 红黑树插入调整:
https://blog.csdn.net/u011240877/article/details/53329023

7. 多线程下HashMap 出现的问题

 多线程put 操作后,get 操作导致死循环,导致cpu100%的现象。主要是多线程同时put 时,如果同时触发了rehash 操作,会导致扩容后的HashMap 中的链表中出现循环节点,进而使得后面get 的时候,会死循环。
 多线程put 操作,导致元素丢失,也是发生在多个线程对hashmap 扩容时。
分析参考:https://blog.csdn.net/lz710117239/article/details/78916092

8. HashTable与HashMap的区别?

 HashTable是一个比较古老的类,继承于Dictionary类,HashMap继承于AbstractMap类;
 方法是Synchronized 的,适合在多线程环境中使用,效率稍低;HashMap 不是线程安全的,方法不是Synchronized 的,效率稍高,适合在单线程环境下使用, 所以在多线程场合下使用的话, 需要手动同步HashMap ,Collections.synchronizedMap()。
 HashTable不允许key和value为null,HashMap允许,因此HashMap需要用containsKey()方法来判断键是否存在。HashMap 中数组的默认大小是16,而且一定是2 的倍数,扩容后的数组长度是之前数组长度的2 倍。HashTable 中数组默认大小是11,扩容后的数组长度是之前数组长度的2 倍+1
 哈希值的使用不同(HashTable使用key.hashCode()&0x7FFFFFFF,0x7FFFFFFF是最大的Int值)。

9. ConcurrentHashMap 的原理

设计目标是在最小化更新争用的同时保持并发可读性(通常是方法get(),但也包括迭代器和相关方法)。次要目标是使空间消耗与java.util.HashMap保持大致相同或更好,并且支持许多线程在空表上的高初始插入速率。在ConcurrentHashMap 中,不允许用null 作为键和值。ConcurrentHashMap 使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。读操作大部分时候都不需要用到锁。只有在size 等操作时才需要锁住整个hash 表。它把区间按照并发级别(concurrentLevel),分成了若干个segment。默认情况下内部按并发级别为16 来创建。对于每个segment 的容量,默认情况也是16。当然并发级别(concurrentLevel)和每个段(segment)的初始容量都是可以通过构造函数设定的。
1.8中放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,结构如下:

详细讲解:https://blog.csdn.net/fouy_yun/article/details/77816587
1.7/8:http://www.importnew.com/28263.html
1.7:详细理解:http://blog.csdn.net/yan_wenliang/article/details/51029372

10. ConcurrentHashMap 存在的问题?

弱一致性的。Get()方法并没有加锁。指定太大的初始化值会内存泄漏堆溢出。

11. 同步容器和并发容器

https://blog.csdn.net/sjjsh2/article/details/53286001
常见的并发容器:
CopyOnWriteArrayList:写时复制的容器,适合读多写少的场景。最终一致性,内存占用大
CopyOnWriteArraySet、ConcurrentHashMap、ConcurrentLinkedQuerue
ConcurrentSkipListMap:key有序,高并发。可以在高效并发中替代SoredMap
ConcurrentSkipListSet(可以在高效并发中替代SoredSet)
常见的同步容器:
Vector、Stack、HashTable、Collections类中提供的静态工厂方法创建的类

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值