HashMap线程安全问题
在分析HashMap线程安全问题之前,我们先看看什么是线程安全,线程安全问题是怎么引起的
一、什么是线程安全
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
百度百科
当多个线程同时对同一资源(变量)进行操作时,可能出现数据结果和我们预期结果不一致的情况,这种情况就是线程不安全的表现
线程安全问题大多是由共享资源引起的,共享资源一般是全局变量或局部变量
有了上面的知识我们再来分析HashMap的线程安全问题
二、HashMap的线程安全问题
我们先来看一下HashMap的存值流程
图片来源于网络
1、数据覆盖
-
发生条件
多线程同时进行put存值
-
具体情况
当两个线程同时进行存储元素时,线程一计算出存储位置并判断当前位置可以正常存储后挂起;线程二开始执行,线程二计算出的存储位置和线程一计算出的位置相同,线程二执行完后,该位置已经有线程二存的值;此时线程一继续执行,线程一执行完毕后,在原来计算出的位置存储了线程一的值,这时线程一存的值就覆盖掉了线程二存的值。
2、get值为null
-
发生条件
数组扩容时get
-
具体情况
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab;
这时resize方法中的两行代码,当一个线程执行完这两行之后挂起,这时map的数组为空,这时get元素为null
3、死循环(JDK7以前)
-
发生条件
多个线程同时rehash对数组扩容
以下是jdk7中HashMap扩容的核心代码
//数据迁移的方法,头插法添加元素
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//for循环中的代码,逐个遍历链表,重新计算索引位置,将老数组数据复制到新数组中去(数组不存储实际数据,所以仅仅是拷贝引用 //而已)
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);
//将当前entry的next链指向新的索引位置,newTable[i]有可能为空,有可能也是个entry链,如果是entry链,直接在链 //表头部插入。
//以下三行是线程不安全的关键
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
正常的扩容流程是依次计算出每个元素在新数组中的hash值,然后使用头插法存到相应的位置,流程如图
多线程下造成死循环的流程如下