💡 核心结论:一句话先记住
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 是怎么悄悄丢数据的:
importjava.util.HashMap;importjava.util.Map;importjava.util.concurrent.CountDownLatch;publicclassHashMapRaceConditionDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{// 初始化一个普通的 HashMapfinalMap<String,String>unsafeMap=newHashMap<>();intthreadCount=2;intopsPerThread=10000;CountDownLatchlatch=newCountDownLatch(threadCount);// 线程 A:狂写数据ThreadthreadA=newThread(()->{for(inti=0;i<opsPerThread;i++){unsafeMap.put("ThreadA-"+i,"Value-"+i);}latch.countDown();});// 线程 B:也狂写数据ThreadthreadB=newThread(()->{for(inti=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:
importjava.util.Map;importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.CountDownLatch;publicclassConcurrentHashMapSafeDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{// ⭐ 唯一的区别:换成高并发战神 ConcurrentHashMapfinalMap<String,String>safeMap=newConcurrentHashMap<>();intthreadCount=2;intopsPerThread=10000;CountDownLatchlatch=newCountDownLatch(threadCount);ThreadthreadA=newThread(()->{for(inti=0;i<opsPerThread;i++){safeMap.put("ThreadA-"+i,"Value-"+i);}latch.countDown();});ThreadthreadB=newThread(()->{for(inti=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,锁粒度细性能高!