目录
先看多线程下jdk1.8之前hashmap的put方法为什么造成死循环?
Java的HashMap是非线程安全的,在多线程下应该使用ConcurrentHashMap。感谢Doug Lea老爷子的concurrent包。
先看多线程下jdk1.8之前hashmap的put方法为什么造成死循环?
以下分析在JDK1.7前提下
为什么会发生死循环?
主要还是因为HashMap的底层数据结构是数据+链表的结构,因为是链表,就有可能在多线程环境下形成闭合的链路(也就是一个环),这样只要有线程对这个HashMap进行get操作就会产生死循环,本文只分析是如何形成环的。
首先对HashMap对象进行put操作的时候会先检查,假如size >= threshold,(threshold=length*loadFactor,length是当前HashMap的容量,loadFactor是负载因子,size是实际键值对数量),就应该扩容,也就是调用resize(),resiz()里有个transfer()是造成死循环的源头。多个线程同时往HashMap添加新元素时,多个线程并发执行resize会有一定概率出现死循环。
单线程环境下
首要要搞懂单线程是怎么运行的是吧
/**
* Transfers all entries from current table to newTable.
*/
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);//根据算法算出当前值在新表中的hashcode,这里为3
e.next = newTable[i];//newTable[3]的初始值为null,这样e.next就为null,旧表也变了
newTable[i] = e;//新表newTable[3]的数据就是key=3对应的数据块
e = next;//e就指向key=7的数据块了
}
}
}
图解:
多线程环境下
比如此时有两个线程t1,t2执行对同一个HashMap对象执行resize()操作,有一种情况就是,t1先把e和next指向内存中的对象,然后便挂起了,接着t2正常执行了一遍resize(),这时候t1继续执行,但是这时候e和next虽然指向的还是相同的元素,但已经不是指向原先的那个旧的oldTable了,而是指向t2执行完后的那个newTable,因为resize()的时候采用链表的头插法,这时候,next所指的元素的next是e,而不是e的next就是next。
/**
* Transfers all entries from current table to newTable.
*/
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;//线程t1执行完这一句后便挂起了
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;
}
}
}
到此线程1再回来执行的时候就会造成环了。
其实造成环的原因还有很多,就不说啦,(太麻烦啦)
举一种比较简单的情况:
再来看看jdk1.8之后如何解决这个死循环
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
其实就是声明两对指针,维护两个链表,依次在末端添加新的元素。虽然解决了死循环问题,但还是会有其他问题,所以多线程还是尽量用ConcurrentHashMap。
这里比较难理解的是 (e.hash & oldCap) == 0 这一句,但是这个和解决死循环没什么关系。
这里的操作就是 (e.hash & oldCap) == 0 这一句,这一句如果是true,表明(e.hash & (newCap - 1))还会和 e.hash & (oldCap - 1)一样。因为oldCap和newCap是2的次幂,并且newCap是oldCap的两倍,就相当于oldCap的唯一一个二进制的1向高位移动了一位
, (e.hash & oldCap) == 0就代表了(e.hash & (newCap - 1))还会和e.hash & (oldCap - 1)一样。
比如原来容量是16,那么就相当于e.hash & 0x1111 (0x1111就是oldCap - 1 = 16 - 1 = 15),现在容量扩大了一倍,就是32,那么rehash定位就等于e.hash & 0x11111 (0x11111就是newCap - 1 = 32 - 1 = 31)现在(e.hash & oldCap) == 0就表明了e.hash & 0x10000 == 0,这样的话,不就是已知: e.hash & 0x1111 = hash定位值Value并且 e.hash & 0x10000 = 0那么 e.hash & 0x11111 不也就是原来的hash定位值Value吗?
那么就只需要根据这一个二进制位就可以判断下次hash定位在哪里了。将hash冲突的元素连在两条链表上放在相应的位置不就行了嘛。
参考:
https://blog.csdn.net/dingjianmin/article/details/79780350
https://www.cnblogs.com/RGogoing/p/5285361.html
https://blog.csdn.net/mymilkbottles/article/details/76576367