哈希数组
java1.7的hashmap形状如下图
可以看到有一个table数组,数组中每个元素都是一个链表的头。这个数组的长度与加载因子决定扩容机制,具体算法可以查找网上的内容。每个链表视为一个桶,桶里放的node都是经过hash算法后指向统一位置的元素,这个桶是解决哈希冲突的容器。
头插法
此时如果有一个NodeM,经过hash计算,它应该放到table[0]这个位置,头插法最终的结果是NodeM->Node1->Node2->Node3->NULL
扩容
当一个新元素被put进hashmap后,通过计算,已经超过了阈值,此时需要重新创建一个table,并把之前的元素一个一个的放到新table中,我们假设原来table[0] 这个桶,在新表的位置还是table[0],由于是头插法,需要一个一个的插入,顺序是这样的
- Node1->NULL
- Node2->Node1->NULL
- Node3->Node2->Node1->NULL
- …
可以发现和之前的顺序是相反的,具体算法如下,实际上就是SWAP函数
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
简单说明一下怎么个SWAP吧
假设当前桶的情况是这个样子 Node1->Node2->Node3… ,新的table[0] 由于没有数据,此时为NULL
- e= Node1 next=Node2,
e.next = newTable[0]; // 这句话是把Node1的next指向newTable[0],即NULL
newTable[i] = e;// 把Node1 放到newTable[0]的位置
最后的结果是Node1->NULL; - 此时e=Node2,next=Node3
e.next = newTable[0]; // 这句话是把Node2的next指向newTable[0],即Node1
newTable[i] = e;// 把Node2 放到newTable[0]的位置
最后的结果是Node2->Node1->NULL;
死循环
简单的说死循环就是链表中出现了一个环
比如桶是Node1->Node2------->Node3,如果Node1和Node2发生了环,后续对table[0] 进行遍历时就会出现cpu告警
具体解释可以参考死循环的演示,理解《死循环演示》这篇文章的关键是两个线程同时调用hashmap时,都会在自己的私有空间创建一个newTable,第二个线程执行完毕后,也就是把节点逆序后会把它创建的newTable赋值给原table,在resize()函数中有这句话table = newTable。第一个线程在获取原table的各节点顺序时,都会按照这个newTable调整好的逆序来,并没有像它中断前按照的原Table执行。
数据丢失
这种场景很好理解,两个线程同时put一个元素,此时正好发生扩容,且扩容后这个元素假设需要放到table[m]这个位置,初始时这个位置是NULL,线程1判断e.next =table[m]后中断,此时e.next = NULL;线程2执行完毕后,线程1把自己的元素放入了table[m],同时把自己的next指向NULL,实际是替换了线程2的结果,最终导致数据就是。
最后
这篇文章仅仅是一个开头,hashmap还有覆盖等问题,后续接着写。