JDK1.7 及之前版本,在多线程环境下,HashMap 扩容时会造成死循环和数据丢失的问题。
死循环问题:
这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,使得查询元素的操作陷入死循环而无法结束
为解决这个问题,JDK1.8版本的HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。
但是还是不建议在多线程下使用
HashMap
,因为多线程下使用HashMap
还是会存在数据覆盖的问题。并发环境下,推荐使用ConcurrentHashMap
。
数据丢失问题在 JDK1.7 和 JDK 1.8 中都存在,这里以 JDK1.8 为例。
JDK1.8 后,在 HashMap 中,多个键值对可能会被分配到同一个桶,并以链表或红黑树的形式存储。多个线程对 HashMap 的 put 操作会导致线程不安全,具体来说会有数据覆盖的风险。
举个例子:
- public V put(K key, V value) {
- return putVal(hash(key), key, value, false, true);
- }
-
- final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
- boolean evict) {
- // ...
- // 判断是否出现 hash 碰撞
- // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
- if ((p = tab[i = (n - 1) & hash]) == null)
- tab[i] = newNode(hash, key, value, null);
- // 桶中已经存在元素(处理hash冲突)
- else {
- // ...
- }
还有一种情况是这两个线程同时 put 操作导致 size 的值不正确,进而导致数据覆盖的问题:
- public V put(K key, V value) {
- return putVal(hash(key), key, value, false, true);
- }
-
- final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
- boolean evict) {
- // ...
- // 实际大小大于阈值则扩容
- if (++size > threshold)
- resize();
- // 插入后回调
- afterNodeInsertion(evict);
- return null;
- }
所以在并发操作中一般不使用 HashMap ,如果需要使用 Map 的话 可以使用 CurrentHashMap。