💡 核心结论:一句话先记住

HashMap 是个“单线程傲娇怪”,多线程一哄而上绝对会出事! 老版本(JDK 1.7)多线程用它会直接让系统卡死(死循环),新版本(JDK 1.8)虽然不卡死了,但会悄悄丢数据(数据覆盖/全线程不安全)


🛑 两个版本的具体“翻车现场”

1. 老版本(JDK 1.7):致命的“死循环”(CPU 100%)

  • 怎么发生的: 老版本的 HashMap 扩容时用的是“头插法”(新来的数据坐最前面)。如果有两个线程同时给它扩容,你推我抢之间,链表的指针就会被指反,最后自己咬住自己的尾巴,连成了一个环。
  • 后果: 后面只要有人来找数据(执行 get()),就会在这个环里无限绕圈、转到晕厥。表现出来就是服务器的 CPU 瞬间飙到 100%,整个服务直接瘫痪假死。

2. 新版本(JDK 1.8):低调的“数据覆盖”(悄悄丢数据)

  • 怎么发生的: 官方在新版本换成了“尾插法”(乖乖去排队),虽然修复了死循环,但由于没有任何加锁保护,多线程并发 put 时仍然存在严重的数据碰撞覆盖和 size 计数缺失问题。
  • 后果: 连个报错都没有,数据丢得无影无踪,非常阴险。
💻 翻车现场源码级复现:

我们可以用一段简单的 Java 代码,亲眼看看 JDK 1.8 里 HashMap 是怎么悄悄丢数据的:

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;

public class HashMapRaceConditionDemo {
    public static void main(String[] args) throws InterruptedException {
        // 初始化一个普通的 HashMap
        final Map<String, String> unsafeMap = new HashMap<>();
        int threadCount = 2;
        int opsPerThread = 10000;
        
        CountDownLatch latch = new CountDownLatch(threadCount);

        // 线程 A:狂写数据
        Thread threadA = new Thread(() -> {
            for (int i = 0; i < opsPerThread; i++) {
                unsafeMap.put("ThreadA-" + i, "Value-" + i);
            }
            latch.countDown();
        });

        // 线程 B:也狂写数据
        Thread threadB = new Thread(() -> {
            for (int i = 0; i < opsPerThread; i++) {
                unsafeMap.put("ThreadB-" + i, "Value-" + i);
            }
            latch.countDown();
        });

        threadA.start();
        threadB.start();
        latch.await(); // 等待两个线程都作妖结束

        // 理论上应该有 20000 条数据
        System.out.println("【理论期待大小】: " + (threadCount * opsPerThread));
        System.out.println("【实际 Map 大小】: " + unsafeMap.size());
        
        if (unsafeMap.size() < (threadCount * opsPerThread)) {
            System.err.println("🚨 警告:数据对不上!发生了并发覆盖,部分数据悄悄蒸发了!");
        }
    }
}

🛠️ 正确的替代方案(怎么抄作业?)

既然 HashMap 在并发场景这么不靠谱,我们该用谁?

1. ConcurrentHashMap(绝对首选 ⭐⭐⭐⭐⭐)

高并发战神。 JDK 1.8 中它采用了 Node 数组 + 链表 / 红黑树 的结构,并利用 CAS + synchronized 进行了细粒度锁(只锁当前槽位/格子)的设计。既保证了绝对的安全,速度还飞快。

2. Collections.synchronizedMap(低并发备选 ⭐⭐)

相当于给普通的 HashMap 强行配了个粗鲁的保安,管你访问哪个格子,通通把整个 Map 锁住(对象锁),一次只放一个人进去,安全但排队很慢。

3. Hashtable(直接淘汰 ❌)

上个世纪的老古董,方法全加了 synchronized,又慢又土,现代 Java 开发直接无视它。

💻 抄作业正确姿势代码:

将上面的高并发翻车代码,无缝切换为安全高效的 ConcurrentHashMap

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

public class ConcurrentHashMapSafeDemo {
    public static void main(String[] args) throws InterruptedException {
        // ⭐ 唯一的区别:换成高并发战神 ConcurrentHashMap
        final Map<String, String> safeMap = new ConcurrentHashMap<>();
        int threadCount = 2;
        int opsPerThread = 10000;
        
        CountDownLatch latch = new CountDownLatch(threadCount);

        Thread threadA = new Thread(() -> {
            for (int i = 0; i < opsPerThread; i++) {
                safeMap.put("ThreadA-" + i, "Value-" + i);
            }
            latch.countDown();
        });

        Thread threadB = new Thread(() -> {
            for (int i = 0; i < opsPerThread; i++) {
                safeMap.put("ThreadB-" + i, "Value-" + i);
            }
            latch.countDown();
        });

        threadA.start();
        threadB.start();
        latch.await();

        // 无论运行多少次,结果永远稳如老狗
        System.out.println("【理论期待大小】: " + (threadCount * opsPerThread));
        System.out.println("【实际 Map 大小】: " + safeMap.size());
        System.out.println("🛡️ 结果安全:一条数据都没丢!");
    }
}

🎯 终极秒记口诀

1.7 头插扩容会反转,并发形成环形链,CPU 飙升服务挂!
1.8 尾插顺序不变了,死循环虽修复,数据覆盖仍存在!
解决方案:并发就用 ConcurrentHashMap,锁粒度细性能高!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐