目录
目标
不去讲述hashmap的作用,而是列举个人认为的重点,如通过源码解释为什么如此设计。
JDK版本
JDK8
个人认为的重点
Node模型
以下是HashMap Node模型 field的定义
final int hash;
final K key;
V value;
Node<K,V> next;
可以发现value和next没有任何修饰符,这点可以与ConcurrentHashMap进行对比,突出可见性的缺失。
hash是key.hashCode经过移位与或非操作得到的,便于采用hash&(tablength-1)方式得到index,即在数组中的位置。
缺失可见性和安全性
由于没有使用同步锁,导致并发访问时缺失可见性和安全性,具体表现为:
缺失可见性 读+写
无法立即读取到最新数据,但处理器缓存和内存同步后可获取到最新数据。
缺失安全性 写+写
put和put同时操作一个链
- 对同一个node修改,造成数据覆盖
- 均在尾部进行插入,造成数据丢失
put和remove同时操作一个链
- 修改和删除针对同一个node,数据丢失
- 尾部插入和删除尾部node同时发生,数据丢失
remove和resize同时操作一个链
- 删除尾部node时,造成未被真的删除
- 删除中间node时,造成未被真的删除
resize和resize同时操作一个链,形成环
- 导致问题:一旦链上形成环路,执行resize的线程以及以后所有需要遍历该链的操作对应线程均进入无限循环,频繁进行出入栈操作,导致CPU飙升。
数据结构改变
put过程中,如果链中元素数量已经超过7,则链转换为红黑树。
为了提升查询效率,降低查询时间复杂度。链表O(n),红黑树O(logN)
新Node放在尾部
源码如下
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
需要说明一点,jdk8之前的源码不是插入到尾部,所以讨论问题时必须明确版本。
个人认为出于以下原因,才在尾部插入Node:
-
遍历完成正到达最后节点
-
仅需要为最后节点的next赋值,操作最少
扩容2倍
扩容2倍这个现象大家都清楚,那为什么这么做呢?接下来从源码角度,阐述个人认为的原因:
移位操作更快捷
oldCap<<1,向左移动一位且末位补0,即二进制增加一个高位,相当于*2.
例如16<<1,二进制表示(16)10000<<1 = (32)100000
resize过程确定index
确定扩容后node所在index的规则:oldCap&node.hash,0则放在原oldIndex,否则放在(oldIndex+oldCap)
举例:
oldIndex区间为[0,oldCap-1),此时某node的oldIndex为1
resize过程中,确定该node在新数组的index:
若(oldCap)10000&(node.hash)xxx10001=00010000,不为0,则对应index为(oldCap)10000+oldIndex(0001)=10001=17,即tab[17];
若(oldCap)10000&(node.hash)xxx00001=00000000,为0,则对应index不变,即tab[1]
定位index
node.hash&(length-1)确定数组index