最近在学习并发,看到书上写到hashmap在并发执行put操作时会引起死循环,因为在put中会引起扩容操作,使链表形成环形的数据结构,不是很明白,然后在网上看了一些博客,但是很多博客都是jdk1.7版本的,而1.8版本中的扩容操作已经和1.7版本中大不一样了,于是自己开始研究,看源码的时候,觉得jdk1.8版本中多线程put不会在出现死循环问题了,只有可能出现数据丢失的情况,因为1.8版本中,会将原来的链表结构保存在节点e中,将原来数组中的位置置为null,然后依次遍历e,根据hash&n是否等于0,分成两条支链,保存在新数组中。如果有新的线程继续添加元素,则扩容操作中可能就会取到null值造成数据丢失,后来线程返回的扩容以后的数组将替换掉前面有可能正确进行扩容的数组。jdk1.7版本中,扩容过程中会新数组会和原来的数组有指针引用关系,所以将引起死循环问题。
jdk1.8扩容代码
final Node<K,V>[] resize() {
//oldTab保存原来的table
Node<K,V>[] oldTab = table;
//原来数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//原来数组的阈值
int oldThr = threshold;
//新数组的阈值
int newCap, newThr = 0;
if (oldCap > 0) { //这种情况下应该是数组已经被初始化了不为null
if (oldCap >= MAXIMUM_CAPACITY) {
//已经达到最大的初始容量了,则不扩容了,返回原来的数组
threshold = Integer.MAX_VALUE;
return oldTab;
}
//old<<2,数组扩大为原来的两倍,阈值也扩大为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//省略了部分初始化数组的代码,下面的一段代码是扩容以后进
//行扩容操作的代码
if (oldTab != null) {
//依次遍历数组中的元素
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//首先将桶中的值保存在e节点中
//依次将桶中的元素置为null,
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
//如果不是树节点并且有多个节点的情况下
// 将同一桶中的元素根据(e.hash & oldCap)是否为0进
//行分割,分成两个不同的链表,完成rehash
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;
}
}
}
}
}
return newTab; //返回新数组
}
测试代码1:
public class TestHashMap {
private static HashMap< Integer, Integer > map = new HashMap<>(2);
public static void main(String[] args) throws InterruptedException {
//线程1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 100000; i++) {
int result = i;
new Thread(new Runnable() {
@Override
public void run() {
map.put(result, result);
}
}, "ftf" + i).start();
}
}
});
t1.start();
//让主线程睡眠5秒,保证线程1和线程2执行完毕
Thread.sleep(5000);
for (int i= 1; i <= 100000; i++) {
//检测数据是否发生丢失
Integer value = map.get(i);
if (value==null) {
System.out.println(i + "数据丢失");
}
}
System.out.println("end...");
}
}
此时插入100000条数据,没有引起死循环和数据丢失
继续增大数据量:
此时插入100000条数据,没有引起死循环和数据丢失
继续增大数据量:数据增加到1000000,出现java.lang.OutOfMemoryError,栈内存溢出,重新调整jvm栈区内存的大小
如何调整:深入理解jvm书中写到,如果是建立过多线程导致内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,只能通过减少最大堆和减少栈容量来换取更多的线程。
在idea中修改jvm参数:-Xss120k,减少一个线程分配的栈内存(默认为1m)
调整以后,成勋正常运行,并且出现数据丢失现象,但是仍没有出现死循环现象
运行结果部分:
value:null数据丢失
value:null数据丢失
value:null数据丢失
value:null数据丢失
value:null数据丢失
end...
出现了很多null值,这里只是一部分
仅是皮皮甜个人观点,如有错误,欢迎指正