
全文目录开篇语0. 前言那年我在生产环境踩过的坑 1. 为什么要抛弃 HashMap 和 Hashtable以前的痛 HashMap由于“太快”而“失控”的野马 Hashtable穿着防弹衣的蜗牛2. JDK 1.7 时代的 ConcurrentHashMap分段锁的智慧 3. JDK 1.8 的大变革放弃分段死磕 CAS Synchronized 核心架构升级️ 黑科技CAS synchronized4. 哪怕是全栈也得看代码Talk is cheap, show me the code! 5. 多维度对比总结一张表看懂江湖地位 6. 结语别让基础成为你的天花板 文末开篇语哈喽各位小伙伴们你们好呀我是喵手。运营社区C站/掘金/腾讯云/阿里云/华为云/51CTO欢迎大家常来逛逛今天我要给大家分享一些自己日常学习到的一些知识点并以文字的形式跟大家一起交流互相学习一个人虽可以走的更快但一群人可以走的更远。我是一名后端开发爱好者工作日常接触到最多的就是Java语言啦所以我都尽量抽业余时间把自己所学到所会的通过文章的形式进行输出希望以这种方式帮助到更多的初学者或者想入门的小伙伴们同时也能对自己的技术进行沉淀加以复盘查缺补漏。小伙伴们在批阅的过程中如果觉得文章不错欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持0. 前言那年我在生产环境踩过的坑 说实话提起并发容器我脑海里总是回荡着几年前那个深夜报警电话的铃声。那时候年轻气盛觉得HashMap天下第一快就完事了。结果呢在一个多线程高并发的统计业务里直接给我整出了个 CPU 100% 的死循环那是 JDK 1.7 时代的眼泪啊要么就是数据莫名其妙地丢了。当时我就在想要是早点把这并发容器的底裤扒干净我至于大半夜在那儿重启服务器吗咱们做开发的特别是搞全栈的不仅要会写前端那花里胡哨的页面后端的底层逻辑更是得硬今天咱们就来一场“颅内手术”把ConcurrentHashMap以下简称 CHM从 1.7 到 1.8 的演变以及它为啥能吊打Hashtable给它剖析得明明白白别眨眼全是细节✨1. 为什么要抛弃 HashMap 和 Hashtable以前的痛咱先得把背景交代清楚不然直接上原理那是耍流氓。 HashMap由于“太快”而“失控”的野马HashMap是咱们最常用的但它不是线程安全的。你想象一下你和你的怨种同事同时在这个 Map 里写数据。在JDK 1.7里因为它是头插法扩容的时候一旦并发链表很容易形成环以后你再get嘿嘿死循环等着你CPU 直接起飞在JDK 1.8虽然改成尾插法解决了死循环但多线程下还是会覆盖数据。你辛辛苦苦存进去的 100 块钱别人一个线程过来覆盖了钱没了这谁能忍 Hashtable穿着防弹衣的蜗牛于是有人说了“用Hashtable啊它安全”哎哟喂兄弟这都 202X 年了。Hashtable确实安全但它太暴力了它在所有关键方法上都加了synchronized而且是锁住整个对象。这就好比咱们去超市结账整个超市不管有多少个收银台一次只允许一个人进超市买东西、结账。只要我在里面你们都在外面晒太阳排队等着。这吞吐量能高才怪所以我们需要一个**既能抗揍线程安全又能跑得快高并发**的超级英雄。这就是ConcurrentHashMap诞生的意义2. JDK 1.7 时代的 ConcurrentHashMap分段锁的智慧 在 JDK 1.7 时期设计者 Doug Lea 大神Java 并发之父想了个绝招既然锁整个超市太慢那我就把超市分成 16 个区Segment不就完了这也就是经典的“分段锁”Segmented Locking理论。结构一个ConcurrentHashMap内部维护了一个Segment数组。每个Segment本质上就是一把锁继承了ReentrantLock而且每个Segment里面装着一个小的HashEntry数组类似于一个小HashMap。原理当你要put数据时先根据 Key 算出你在哪个区Segment。如果你去 1 区买可乐他在 2 区买薯片咱们互不干扰并行执行爽不爽只有当咱俩都非要去 1 区抢同一包辣条时才需要竞争那把锁。并发度默认是 16。也就是说理想情况下它支持 16 个线程同时写比Hashtable那个“独苗”强了 16 倍啊但是注意转折来了这种设计也有软肋。你想啊虽然分了区但如果你运气不好所有数据都堆到一个区里了呢那不就退化成Hashtable了吗而且查找数据时还得经过两次 Hash一次找 Segment一次找 Entry效率还是有提升空间的。3. JDK 1.8 的大变革放弃分段死磕 CAS Synchronized 到了 JDK 1.8Oracle 的工程师们那是真狠啊直接把 1.7 的代码推翻重写了Segment不要了现在的 CHM看起来更像是一个优化到了极致的HashMap。 核心架构升级1.数据结构Node数组 链表 红黑树。注意当链表长度超过 8 且数组长度超过 64 时链表会转成红黑树。为什么要转因为链表查询是 O(n)红黑树是 O(log n)。即使哈希冲突严重查询效率也能起飞2.锁的粒度细到了极致。不再锁“一段”而是只锁“这一个桶”Bucket的头节点。只要咱俩操作的 Key 不在同一个 Hash 槽位上哪怕是同一个数组里的邻居也互不影响这并发度理论上就是数组的长度啊️ 黑科技CAS synchronized这也是面试最爱问的“为啥 1.8 不用ReentrantLock而用synchronized了”来听我给你掰扯掰扯put的过程你就懂了第一步计算 Hash定位数组下标。第二步如果这个位置是空的null不用锁直接用CAS (Compare And Swap)尝试把数据放进去。CAS 是啥就是无锁原子操作“我看这儿没人我放个东西如果放的时候发现确实没人我就放成功了如果有人抢先了我就重试。” —— 这一步性能极高完全没有线程阻塞⚡️第三步如果这位置有人了Hash 冲突或者正在扩容那没办法必须加锁。这时候锁的是谁锁的是这个链表或红黑树的头节点。用synchronized锁。为啥用synchronized因为 JDK 1.6 之后JVM 对synchronized做了惊天地泣鬼神的优化偏向锁、轻量级锁、锁粗化…在低竞争下它的性能甚至优于ReentrantLock而且它更省内存不用每个节点都继承 AQS 那个庞大的对象。一句话总结1.8 的 CHM 就是“平时用 CAS 裸奔冲突时用 synchronized 护体链表长了变红黑树加速”。完美4. 哪怕是全栈也得看代码Talk is cheap, show me the code! 光说不练假把式。咱们模拟一个场景100 个线程并发统计访问量。如果你用HashMap你会发现最后的数字永远小于预期。用ConcurrentHashMap那就是稳稳的幸福。来上代码这可是我亲手敲的热乎着呢importjava.util.HashMap;importjava.util.Map;importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.CountDownLatch;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;importjava.util.concurrent.atomic.AtomicInteger;/** * 并发容器大比拼 * Author: 那个很帅的全栈开发 */publicclassConcurrencyBattle{// 请求总数privatestaticfinalintTOTAL_REQUESTS10000;// 模拟并发线程数privatestaticfinalintTHREAD_COUNT200;publicstaticvoidmain(String[]args)throwsInterruptedException{// 1. 这种时候用 HashMap 就是作死MapString,IntegerdangerousMapnewHashMap();// 2. 这才是我们的主角MapString,IntegersafeMapnewConcurrentHashMap();// 既然是全栈原子类也得懂吧这里用作对照组AtomicIntegercorrectCountnewAtomicInteger(0);// 开始暴力测试 HashMaptestMap(HashMap (危险),dangerousMap);// 开始测试 ConcurrentHashMaptestMap(ConcurrentHashMap (安全),safeMap);}privatestaticvoidtestMap(Stringname,MapString,Integermap)throwsInterruptedException{// 这里的代码虽然简单但逻辑很深奥哦~ExecutorServiceexecutorExecutors.newCachedThreadPool();CountDownLatchlatchnewCountDownLatch(TOTAL_REQUESTS);longstartSystem.currentTimeMillis();for(inti0;iTOTAL_REQUESTS;i){executor.execute(()-{// ⚠️注意CHM 保证的是线程安全但 map.get map.put 这个组合操作本身不是原子的// 所以在真实业务中我们通常用 atomic 操作或者 compute 方法// 但为了演示最基础的“不丢数据”能力咱们这里稍微加个小锁或者利用 CHM 特性// 咱们这里直接演示最粗暴的覆盖问题看看谁能活下来synchronized(map){// 这里为了模拟单纯的数据插入完整性稍微偷个懒加了 synchronized// 但如果你去掉 synchronizedHashMap 会丢数据或者抛异常CHM 虽然如果不加锁逻辑上会有覆盖// 但内部结构绝对不会坏// 实际上 CHM 推荐用法是 map.merge() 或 map.compute()map.put(key,map.getOrDefault(key,0)1);}latch.countDown();});}latch.await();executor.shutdown();longendSystem.currentTimeMillis();System.out.println(----------------------------------------);System.out.println(选手: name);System.out.println(最终结果: map.get(key));System.out.println(耗时: (end-start)ms);System.out.println(结论: (map.get(key)TOTAL_REQUESTS?✅ 稳如老狗:❌ 翻车了兄弟));}}这里有个小坑得提醒大家虽然ConcurrentHashMap本身的方法比如put,get是线程安全的但如果你写出if (map.get(k) null) { map.put(k, v) }这种代码这两步操作之间是不安全的这叫“竞态条件”。正确姿势使用putIfAbsent()或者 JDK 8 提供的computeIfAbsent()、merge()等原子方法。咱们做全栈的代码必须要优雅5. 多维度对比总结一张表看懂江湖地位 咱把这几个哥们儿拉在一起做一个全方位的“相亲角”对比方便大家记忆特性HashMapHashtableConcurrentHashMap (1.7)ConcurrentHashMap (1.8)线程安全❌ NO✅ YES✅ YES✅ YES锁机制无全局锁 (synchronized)分段锁 (Segment extends ReentrantLock)CAS synchronized (Node/TreeBin)锁粒度N/A极大 (整个Map)中等 (1/16 Map)极小 (单个Hash桶)扩容不安全 (死循环/丢数据)阻塞所有线程仅当前 Segment 扩容多线程协同扩容 (黑科技)查询效率O(1) / O(log n)O(1)二次 Hash较慢O(1) / O(log n) (红黑树加持)允许 null✅ Key/Value 都行❌ 不行❌ 不行❌ Key/Value 都不行看到没ConcurrentHashMap 1.8 简直就是六边形战士特别是那个“多线程协同扩容”这可是 1.8 的大杀器。当一个线程发现 Map 正在扩容时它不会傻傻等待而是会去帮忙一起迁移数据这设计思路简直绝了6. 结语别让基础成为你的天花板 洋洋洒洒写了这么多其实就想告诉大家一个理儿作为全栈开发者我们很容易沉迷于前端的框架迭代Vue3 出完出 React18学不动了啊喂或者后端各种微服务架构。但往往决定你能不能写出高性能、高可用系统的正是这些看起来枯燥的底层原理。ConcurrentHashMap不仅仅是一个容器它体现了空间换时间、锁粒度细化、CAS 乐观锁、红黑树优化等极其精妙的编程思想。下次面试官再问你“这玩意儿底层怎么实现的”你可以自信地扬起嘴角看着他的眼睛不仅讲出原理还能聊聊 1.7 到 1.8 的演进哲学甚至能吐槽一下Hashtable的笨重。相信我那一刻你就是全场最靓的仔✨… …文末好啦以上就是我这期的全部内容如果有任何疑问欢迎下方留言哦咱们下期见。… …学习不分先后知识不分多少事无巨细当以虚心求教三人行必有我师焉wished for you successed ⭐️若喜欢我就请关注我叭。⭐️若对您有用就请点赞叭。⭐️若有疑问就请评论留言告诉我叭。版权声明本文由作者原创转载请注明出处谢谢支持