众所周知,HashMap不是线程安全的,但是一不小心就可能缺乏同步地用到了多线程环境里去了,那么在没有同步的时候,HashMap可能出现哪些问题呢?
一、put非null元素后get出来的却是null,具体分析如下:
get方法:
public
V get(Object key) {
if
(key ==
null
)
return
getForNullKey();
int
hash = hash(key.hashCode());
// indexFor方法取得key在table数组中的索引,table数组中的元素是一个链表结构,
// 遍历链表,取得对应key的value
for
(Entry e = table[indexFor(hash, table.length)];
e !=
null
; e = e.next) {
Object k;
if
(e.hash == hash && ((k = e.key) == key || key.equals(k)))
return
e.value;
}
return
null
;
}
|
put方法:
public
V put(K key, V value) {
if
(key ==
null
)
return
putForNullKey(value);
int
hash = hash(key.hashCode());
int
i = indexFor(hash, table.length);
for
(Entry e = table[i]; e !=
null
; e = e.next) {
Object k;
if
(e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(
this
);
return
oldValue;
}
}
modCount++;
// 若之前没有put进该key,则调用该方法
addEntry(hash, key, value, i);
return
null
;
}
|
再看看addEntry里面的实现:
void
addEntry(
int
hash, K key, V value,
int
bucketIndex) {
Entry e = table[bucketIndex];
table[bucketIndex] =
new
Entry(hash, key, value, e);
if
(size++ >= threshold)
resize(
2
* table.length);
}
|
里面有一个if块,当map中元素的个数(确切的说是元素的个数-1)大于或等于容量与加载因子的积时,里面的resize是就会被执行到的,继续resize方法:
void
resize(
int
newCapacity) {
Entry[] oldTable = table;
int
oldCapacity = oldTable.length;
if
(oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return
;
}
Entry[] newTable =
new
Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (
int
) (newCapacity * loadFactor);
}
|
resize里面重新new一个Entry数组,其容量就是旧容量的2倍,这时候,需要重新根据hash方法将旧数组分布到新的数组中,也就是其中的transfer方法:
void
transfer(Entry[] newTable) {
Entry[] src = table;
int
newCapacity = newTable.length;
for
(
int
j =
0
; j < src.length; j++) {
Entry e = src[j];
if
(e !=
null
) {
src[j] =
null
;
do
{
Entry next = e.next;
int
i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
while
(e !=
null
);
}
}
}
|
在这个方法里,将旧数组赋值给src,遍历src,当src的元素非null时,就将src中的该元素置null,即将旧数组中的元素置null了,也就是这一句:
if
(e !=
null
) {
src[j] =
null
;
|
此时若有get方法访问这个key,它取得的还是旧数组,当然就取不到其对应的value了。
下面,我们重现一下场景:
import
java.util.HashMap;
import
java.util.Map;
public
class
TestHashMap {
public
static
void
main(String[] args) {
final
Map map =
new
HashMap(
4
,
0
.5f);
new
Thread(){
public
void
run() {
while
(
true
) {
System.out.println(map.get(
"name1"
));
try
{
Thread.sleep(
1000
);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
for
(
int
i=
0
; i map.put(
"name"
+ i,
"value"
+ i);
}
}
}
|
Debug上面这段程序,在map.put处设置断点,然后跟进put方法中,当i=2的时候就会发生resize操作,在transfer将元素置null处停留片刻,此时线程打印的值就变成null了。
其它可能由未同步HashMap导致的问题:
1、多线程put后可能导致get死循环(主要问题在于put的时候transfer方法循环将旧数组中的链表移动到新数组)
2、多线程put的时候可能导致元素丢失(主要问题出在addEntry方法的new Entry
总结:HashMap未同步时在并发程序中会产生许多微妙的问题,难以从表层找到原因。所以使用HashMap出现了违反直觉的现象,那么可能就是并发导致的了