多线程情况下,HashMap线程不安全环节源码分析:
1、首先需要了解一下JDK7中HashMap的存储结构(图片来自某大神博客):
2、多线程情况下使用hashMap的put方法,源码如下
//向hashMap中添加Entry
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);//扩容2倍
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
/**
* @param hash 通过key值计算出的值
* @param key
* @param value
* @param bucketIndex 通过hash值计算出的table中的entry数组的位置
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];//保存之前该位置的entry值
table[bucketIndex] = new Entry<>(hash, key, value, e);//new一个新的entry挂在table中相同位置的entry的next上
size++;
}
假设线程A、B同时向hashMap中添加元素,根据源码可以看出每次添加一个新的元素都会创建一个新的Entry,根据源码可以看出,每次创建新的entry的时候都会根据key值计算出hash值,然后根据hash值计算出bucketIndex值。如果两个线程同时传入的数据计算出的hash值相同,就会导致其中一个线程的数据丢失。
3、hashMap扩容
hashMap扩容机制:创建一个新的table设置容量为你定义的容量,然后通过transfer方法给newtable赋值。transfer方法赋值过程是,首先遍历之前的table,根据之前table中的各个entry中hash值和newTable的容量重新计算entry在newTable中的位置,完成扩容。
多线程情况下,扩容机制线程不安全:
情境一:假设有两个线程A,B同时访问同一个HashMap并同时进入resize方法,线程A根据自己的需要重新hash计算了各个entry在table中的位置,线程B根据自己需要做了同样的操作,那么只有最后一个访问的线程的扩容操作生效,其余线程的数据会丢失。
情境二:假设线程A,B同时访问同一个HashMap并同时进入resize方法,对于每个线程在任意语句都可能被挂起,待其他线程执行执行若干语句后再被调度,线程A在执行resize的时候挂起,在次过程中线程B完成了resize,table结构中可能会出现链表环,一旦使用get读取数据就会出现死循环。
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {//判断当前table的容量是否为最大容量,如果是则不进行扩容
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/**
*
* @param newTable 新的table数组
* @param rehash 是否重新计算entry的hash值,返回boolean类型数据
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {//遍历之前的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);//根据之前的hash值重新计算entry变量存放在new table中的位置
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
高并发情况下最好使用concurrentHashMap,或者使用Collections.synchronizedMap包装一下hashMap