目录
1. HashMap是如何实现原理?
2. HashMap采用的hash算法是什么?
3. 为什么map进行2倍扩容?
4. HashMap 的扩容机制?
5. 为什么要引入红黑树?
6. 红黑树专题
7. 多线程下HashMap 出现的问题
8. HashTable与HashMap的区别?
9. ConcurrentHashMap 的原理?
10. ConcurrentHashMap 存在的问题?
11. 同步容器和并发容器
是一个散列表,它存储的内容是键值对(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,会导致插入的两个对象实际上是相同的,但是还是能够插入成功。
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值尽可能的分散,减小碰撞的几率。
这个方法非常巧妙,它通过h & (table.length -1) 来得到该对象的保存位,保证获取的index一定在数组范围内。而HashMap 底层数组的长度总是2 的n 次方,2n-1 得到的二进制数的每个位上的值都为1,那么与全部为1 的一个数进行与操作,速度会大大提升。当length 总是2 的n 次方时,h& (length-1)运算等价于对length 取模,也就是h%length,但是&比%具有更高的效率。还有扩容后结点的新位置可以计算出来。
当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”。如图:
JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。
当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。
针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。而且红黑树查找性能稳定。
为什么要多于8个结点才用红黑树呢?
因为红黑树需要进行左旋,右旋操作,增删复杂。而单链表简单。如果元素小于8个,链表就足够好了。随着结点增多,链表查询和修改删除成本增高。因此用红黑树优化。
1) 什么是红黑树?
红黑树本质上是一种二叉查找树,但它在二叉查找树的基础上额外添加了一个标记(颜色),同时具有一定的规则。这些规则使红黑树保证了一种平衡,插入、删除、查找的最坏时间复杂度都为 O(logn)。
它的统计性能要好于平衡二叉树(AVL树),因此,红黑树在很多地方都有应用。比如在 Java 集合框架中,很多部分(HashMap, TreeMap, TreeSet 等)都有红黑树的应用,这些集合均提供了很好的性能。
在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3 每个叶节点(NIL节点,空节点)是黑色的。
性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。
2) 红黑树插入调整:
https://blog.csdn.net/u011240877/article/details/53329023
多线程put 操作后,get 操作导致死循环,导致cpu100%的现象。主要是多线程同时put 时,如果同时触发了rehash 操作,会导致扩容后的HashMap 中的链表中出现循环节点,进而使得后面get 的时候,会死循环。
多线程put 操作,导致元素丢失,也是发生在多个线程对hashmap 扩容时。
分析参考:https://blog.csdn.net/lz710117239/article/details/78916092
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值)。
设计目标是在最小化更新争用的同时保持并发可读性(通常是方法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
弱一致性的。Get()方法并没有加锁。指定太大的初始化值会内存泄漏堆溢出。
https://blog.csdn.net/sjjsh2/article/details/53286001
常见的并发容器:
CopyOnWriteArrayList:写时复制的容器,适合读多写少的场景。最终一致性,内存占用大
CopyOnWriteArraySet、ConcurrentHashMap、ConcurrentLinkedQuerue
ConcurrentSkipListMap:key有序,高并发。可以在高效并发中替代SoredMap
ConcurrentSkipListSet(可以在高效并发中替代SoredSet)
常见的同步容器:
Vector、Stack、HashTable、Collections类中提供的静态工厂方法创建的类