跳到主要内容

Java并发编程学习五:并发容器

一、HashMap为什么线程不安全?

HashMap是Map的主要实现类之一,但是它并不具备线程安全的特点。

以HashMap的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<K,V> 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++ 是一个复合操作
modCount++;

addEntry(hash, key, value, i);
return null;
}

且不去具体分析put方法的内部逻辑,单看代码中的modCount++操作,前面在对线程安全的学习部分,就提到过这个操作并不是原子操作,它的实际执行步骤有三步,每一步操作都有可能被打断。

除了非原子操作,HashMap还有什么原因导致它是线程不安全的呢?

1. 扩容时获取值可能为null

HashMap 本身默认的容量不是很大,如果不停地往 map 中添加新的数据,它便会在合适的时机进行扩容。而在扩容期间,它会新建一个新的空数组,并且用旧的项填充到这个新的数组中去。那么,在这个填充的过程中,如果有线程获取值,很可能会取到 null 值,而不是原来添加的值

public class HashMapNotSafe {

public static void main(String[] args) {

final Map<Integer, String> map = new HashMap<>();

final Integer targetKey = 0b1111_1111_1111_1111; // 65 535
final String targetValue = "v";
map.put(targetKey, targetValue);

new Thread(() -> {

IntStream.range(0, targetKey).forEach(key -> map.put(key, "someValue"));
}).start();

while (true) {

if (null == map.get(targetKey)) {

throw new RuntimeException("HashMap is not thread safe.");
}
}
}
}