看《Java并发编程的艺术》到第六章了,讲并发框架的,里面提到了使用HashMap会出现循环引用的情况,搜了下,HashMap1.7在扩容时采用头插法,会成环,导致多线程的场景下容易出现死循环;然后又听说1.8的红黑树也会出现成环的问题…就有点好奇hashmap内部是咋样的了,
俺这边没有jdk8,7的环境。就看看11的吧(虽然1.8那个bug我是一直没复现出来)
先看一下UML吧,好像是有点复杂的亚子,一点一点看吧
静态属性
从上到下解释
- 第一个不用解释吧,序列号
- DEFAULT_INITIAL_CAPACITY:初始筒容量,为16
- MAXIMUM_CAPACITY:最大容量,2^30次方,因为这是int类型最大的2的幂次方了。(至于为啥要保持2的幂次方后面再解释)
- DEFAULT_LOAD_FACTOR:负载系数,当使用容量和总容量的比值超过该值时,便需要扩容了。默认为0.75,可以自己设置,选0.75是一个空间与时间的平衡。
- TREEIFY_THRESHOLD:超过该值就会从链表转换为红黑树,为8,这个是根据泊松分布得来的,因为默认调整值为0.75,一个良好的哈希分布函数,桶中Node的分布频率满足泊松分布,为8的可能性已经很小很小了,如果为8了,说明这是一种比较极端的情况(正常情况下扩容就完事了),Map中的分布可能不是很良好,这时便需要将链表转换为红黑树,以空间换时间(TreeNode所占内存是链表节点的两倍)。
- UNTREEIFY_THRESHOLD:当节点低于这种情况时,红黑树转换为链表,为6.
- MIN_TREEIFY_CAPACITY:最小得满足这个桶数量才能使链表转换为树,为了防止转换为树和扩容的冲突。64
类属性
- table:桶,长度为2的幂次方
- entrySet:键值对组成的set。
- size:当前key-value节点数
- modCount:如果在对节点进行修改的时候这个值就会自增,如果这时候正好有线程在遍历操作(或者类似的操作),这时就会抛ConcurrentModificationException
- threshold:当前最大容量
- loadFactor:负载因子
初始化
有仨构造函数
1.空参 2.含初始容量 3.带初始容量和设置的负载参数。4.传入一个Map
前两个都是调用第三个实现的,那我们直接看第三个吧。
就是判断下是否非法(长度位负数)是则抛异常
如果超出允许的最大值了则设为最大值
然后再将真正的容量设为initialCapacity的最接近的大于或者等于的2的幂次方。
public HashMap(int initialCapacity, float loadFactor) {
//初始容量小于0,那还装啥,抛异常
if (initialCapacity < 0)
throw new IllegalArgumentException
("Illegal initial capacity: " + initialCapacity);
//如果设置的初始容量太大了设置为允许的最大值(2^30)
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//负载参数不能小于0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//将合法的负载参数赋予给类变量
this.loadFactor = loadFactor;
//将初始化容量转换为二次幂
//里面用了个Integer.numberOfLeadingZeros方法
//来求(31-传入数字的二进制最高位数)(x)
//【如果传入为负数返回的就会是个负数了】
//再将-1>>>x位即得到最接近且大于等于传入数字的2的幂次方
this.threshold = tableSizeFor(initialCapacity);
}
第四个构造函数
将一个map作为参数传入,将它们存入本map中。
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
//将m转化为键值对插入map
putMapEntries(m