从刚开始学习java,就觉得HashMap底层实现原理是一个非常高大上的问题,以至于从开始接触到现在2年时间过去了,都没有详细研究过。最近在不断写博客的过程中逐步培养起了源码阅读和官方文档阅读的习惯,所以也激起了研究HashMap原理学习的兴趣。
HashMap相关的问题特别多,这也是我们经常对其望而却步的原因。所以,本文不会对HashMap的各个细节问题都进行阐述,这一版会集中解决HashMap的几个关键问题和经常被问到的一些细节知识点。从而搭建起对HashMap一个初级地较为全面的认识,为后续进阶做好准
当map为空时,过程如下
根据初始容量大小,创建出一个对应的数组
计算出元素的hash值,根据hash值计算出角标,此时角标元素为空,把元素放入对应角标
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
当容量不超,但是两个元素hash冲突时,其过程就是往一个链表或者红黑树中添加元素
当添加完元素后,容量达到临界条件时,会触发扩容,扩容就是resize的过程,后续会有介绍。
putVal方法,添加元素的过程
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果数组为空,则初始化一个数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果数组角标不存在元素,则直接放到数组中
if ((p = tab[i = (n - 1) & hash]) == null) //p就是后面往链表或者树的根节点
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k; //e就是一个临时节点,用于存放目标位置的指针
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) //key已存在
e = p;
else if (p instanceof TreeNode) //桶已经是红黑树的情况
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//桶还是链表的情况
for (int binCount = 0; ; ++binCount) {
//遍历整个桶
if ((e = p.next) == null) {
//桶内无重复的key,直接用尾插法添加
//p.next指针变化不会影响e,注意,不是指针的内容变化,所以e此时还是null
p.next = newNode(hash, key, value, null);
//桶内元素个数达到临街点,对桶进行树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//尚未到最后一个节点,需要逐个元素判重,如果重复,直接e=p.next
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; //尚未匹配到,则p=p.next,继续遍历下一个节点
}
}
if (e != null) {
// existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//空方法,从Map继承的,没有实现。LinkedHashMap会有实现。
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//size增加1,并且判断此时的size是否大于阈值,如果大于,则进行扩容操作。
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
//扩容之后数据如何移动,是本部分的学习重点
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//如果老数组已经是最大值了,则直接放大阈值,不进行扩容操作
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//用<<操作对老容量扩大2倍,之后再对阈值扩大2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else {
// 常规情况,zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({
"rawtypes","unchecked"})
//创建一个新容量的数组,如果容量已经是最大值,可以发现,数组的元素不会往后挪了
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//已经有数据的情况
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果当前节点是树节点,则对一个树中的元素进行遍历判断是否需要移动
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// preserve order
//这里的lo代表桶在迁移之前的原位,hi需要移动的元素的新位置,hi=lo+oldCap
//在迁移的过程中,需要记录每个位置的head和tail,从一个桶分裂成2个桶
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//这一行的作用是判断元素是否需要移动,详细参考说明【1】
if ((e.hash & oldCap) == 0) {
//此时不需要移动
if (loTail == null) //常规操作,链表采用尾插法添加元素,最开始tail和head都是null
loHead = e; //把第一个元素赋值给head
else
loTail.next = e; //如果链表中已经有元素了,则把当前元素赋值给loTail.next。详见【2】
loTail = e; //把当前元素设置为新的链表尾部节点。
}
else {
//此时,当前元素需要移动
if (hiTail == null) //和上文意思相同,就是往链表中追加元素的常规判断
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
//说明原来的位置还有部分元素保留
loTail.next = null; //由于所有元素都遍历完了,最后一个元素的next需要置为空。这是必须的,否则有可能把分裂前的老元素带过来了。
newTab[j] = loHead; //loHead就替换原来数组对应角标的元素,作为桶的根节点
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead; //hiHead的位置就是lo+oldCap,hiHead作为移动后桶的根节点
}
}
}
}
}
return newTab;
}
详细说明
小结
为什么我们会说HashMap是线程不安全的呢?接下来进行分析。首先我们需要明确一个理论基础,所有的线程不安全,都属于三个问题,分别是原子性问题,有序性问题,可见性问题。因此,分析HashMap的线程安全问题,也一定是从这三个角度出发。这也是分析任何线程安全问题一般的方法论。
首先我们来分析原子性问题。
原子性问题的条件是多个线程共享资源,并且对共享资源进行多步操作。HashMap的共享资源是table、bin、size。因此,可能发生以下安全问题
因此,hashMap的原子性问题会带来两个明显的问题,分别是1)size不对;2)数据覆盖从而导致丢失。
由于hashmap中使用了数组,因此,需要确定数组的长度。
详见上一章的resize源码分析
jdk1.8计算hash值的算法如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
原因如下:
hashmap的index=hash&(length-1),相当于只用了hash值的最后几位参与运算,这样一来,相当于hash值变小了,从而波动的范围也变小了,进而导致hash冲突的概率增加。比如数组长度是16,hash值原来长度是32位。那么相当于只要是最后4位相同的所有数,index都会相同。大概算一下冲突的个数就等于2(32-4)=228个。
如果增加了扰动函数,把进行h>>>16操作,那么,即使两个数最后4位相同,如果第17-20位不同,则不会冲突。这样一来,就实现了降低冲突元素个数的效果。
但是,我们一定有一个疑问,会不会原来2个数的低4位不同,进行亦或操作之后,反而相同了呢。以及为什么不用与运算或者或运算呢?在源码中是这样解释的:
//采用异或运算从效率、效果、可用性上综合来看最合适的选择。
There is a tradeoff between speed, utility, and quality of bit-spreading
//其实hashcode的算法已经足够散列,但是由于hashmap中会用tree存大量的数据,为了避免系统性的bug,采用了XOR作为成本最低的一个扰动方式。
Because many common sets of hashes are already reasonably distributed (so don't benefit from spreading), and because we use trees to handle large sets of collisions in bins, we just XOR some shifted bits in the cheapest possible way to reduce systematic lossage
结论:采用高位参与异或运算是一个综合效益最好的选择。核心目的在于避免hashcode可能存在的系统性缺失导致存储大量数据时的冲突问题。
在3种情况下,HashMap会进行扩容: