数据丢失,而且不抛出并发异常,多线程使用HashMap踩坑

B站影视 内地电影 2025-09-04 20:21 1

摘要:在多线程环境中使用 HashMap 进行并发操作时,可能会导致数据丢失或不一致的问题。特别是,HashMap 的 put 方法在并发情况下不会抛出异常,这使得问题更加隐蔽且难以排查。本文将探讨这些问题的根源,并推荐使用 computeIfAbsent 、put

最近踩了一个别人挖的坑,遂写本文。

诚如标题所示,你可能会问为什么会犯这么低级的错误,为什么并发环境下没有使用线程安全类。由于涉及公司业务,我不便透露更多。简单总结原因为以下几点:

项目极其复杂每一个模块、类、方法都看起来没有问题,但是综合起来就有问题了存在上下文的共享状态,使得问题难以排查

在多线程环境中使用 HashMap 进行并发操作时,可能会导致数据丢失或不一致的问题。特别是,HashMap 的 put 方法在并发情况下不会抛出异常,这使得问题更加隐蔽且难以排查。本文将探讨这些问题的根源,并推荐使用 computeIfAbsent 、putIfAbsent 和 merge 等方法来替代直接使用 put 方法,以确保数据的完整性和一致性。

在 Java 中,HashMap 是一个非线程安全的数据结构。当多个线程同时对 HashMap 进行写操作时,可能会导致数据丢失或不一致的情况。特别需要注意的是,HashMap 的 put 方法在并发修改时不会抛出 ConcurrentModificationException ,这使得问题更加难以检测和调试。本文将通过一个示例代码展示这种问题,并提供一些替代方案来解决这些问题。

在多线程环境中使用 HashMap 的 put 方法时,可能会出现数据丢失的情况。以下是一个示例代码:

public static void main(String args) { HashMap map = new HashMap; ConcurrentHashSet threads = new ConcurrentHashSet; IntStream.range(0, 100000).parallel.forEach(x -> { if (threads.add(currentThread)) { System.out.println("currentThread = " + currentThread.getName); } map.put(x, x); }); System.out.println("map.size = " +}

在上述代码中,我们期望 map 的大小为 100,000,但实际输出可能会小于这个值。这是因为 HashMap 在多线程环境下并不是线程安全的。

我的运行环境输出如下:

currentThread = maincurrentThread = ForkJoinPool.commonPool-worker-1currentThread = ForkJoinPool.commonPool-worker-2currentThread = ForkJoinPool.commonPool-worker-5currentThread = ForkJoinPool.commonPool-worker-3currentThread = ForkJoinPool.commonPool-worker-6currentThread = ForkJoinPool.commonPool-worker-4currentThread = ForkJoinPool.commonPool-worker-7map.size = 84920final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node tab; Node p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize; afterNodeInsertion(evict); return null;}

put 方法的具体实现如上,可以看出,HashMap 只在代码最后进行修改操作计数操作,并没有进行计数检查操作,也不可能抛出并发修改异常(ConcurrentModificationException)。因此如果只进行多线程 put 操作,不会有异常,但是数据可能有丢失。

computeIfAbsent 方法可以在键不存在时计算并插入值,确保操作的原子性。

map.computeIfAbsent(key, k -> newValue);

这里不妨看下源码:

public V computeIfAbsent(K key, Function mappingFunction) { if (mappingFunction == null) throw new NullPointerException; int hash = hash(key); Node tab; Node first; int n, i; int binCount = 0; TreeNode t = null; Node old = null; if (size > threshold || (tab = table) == null || (n = tab.length) == 0) n = (tab = resize).length; if ((first = tab[i = (n - 1) & hash]) != null) { if (first instanceof TreeNode) old = (t = (TreeNode)first).getTreeNode(hash, key); else { Node e = first; K k; do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { old = e; break; } ++binCount; } while ((e = e.next) != null); } V oldValue; if (old != null && (oldValue = old.value) != null) { afterNodeAccess(old); return oldValue; } } int mc = modCount; V v = mappingFunction.apply(key); if (mc != modCount) { throw new ConcurrentModificationException; } if (v == null) { return null; } else if (old != null) { old.value = v; afterNodeAccess(old); return v; } else if (t != null) t.putTreeVal(this, tab, hash, key, v); else { tab[i] = newNode(hash, key, v, first); if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); } modCount = mc + 1; ++size; afterNodeInsertion(true); return v;}

可以看出,其执行了并发修改检查。

putIfAbsent 方法在键不存在时插入值,避免覆盖已有值。

map.putIfAbsent(key, newValue);

merge 方法可以在存在键时合并值,提供更灵活的更新策略。

map.merge(key, newValue, (oldValue, newValue) -> oldValue + newValue);

来源:墨码行者

相关推荐