还在用 Hashtable 抗并发?面试官问你 ConcurrentHashMap 底层原理,你真的能答到底裤都不剩吗?

举报
喵手 发表于 2025/12/08 20:37:39 2025/12/08
【摘要】 开篇语哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,...

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区: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.8,Oracle 的工程师们那是真狠啊,直接把 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 的过程,你就懂了:

  1. 第一步:计算 Hash,定位数组下标。

  2. 第二步:如果这个位置是空的(null),不用锁! 直接用 CAS (Compare And Swap) 尝试把数据放进去。

    • CAS 是啥?就是无锁原子操作:“我看这儿没人,我放个东西,如果放的时候发现确实没人,我就放成功了;如果有人抢先了,我就重试。” —— 这一步性能极高,完全没有线程阻塞!⚡️
  3. 第三步:如果这位置有人了(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,那就是稳稳的幸福。

来,上代码!(这可是我亲手敲的,热乎着呢)

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 并发容器大比拼
 * Author: 那个很帅的全栈开发
 */
public class ConcurrencyBattle {

    // 请求总数
    private static final int TOTAL_REQUESTS = 10000;
    // 模拟并发线程数
    private static final int THREAD_COUNT = 200;

    public static void main(String[] args) throws InterruptedException {
        // 1. 这种时候用 HashMap 就是作死
        Map<String, Integer> dangerousMap = new HashMap<>();
        // 2. 这才是我们的主角
        Map<String, Integer> safeMap = new ConcurrentHashMap<>();
        
        // 既然是全栈,原子类也得懂吧?这里用作对照组
        AtomicInteger correctCount = new AtomicInteger(0);

        // 开始暴力测试 HashMap
        testMap("HashMap (危险)", dangerousMap);
        
        // 开始测试 ConcurrentHashMap
        testMap("ConcurrentHashMap (安全)", safeMap);
    }

    private static void testMap(String name, Map<String, Integer> map) throws InterruptedException {
        // 这里的代码虽然简单,但逻辑很深奥哦~
        ExecutorService executor = Executors.newCachedThreadPool();
        CountDownLatch latch = new CountDownLatch(TOTAL_REQUESTS);
        
        long start = System.currentTimeMillis();

        for (int i = 0; i < TOTAL_REQUESTS; i++) {
            executor.execute(() -> {
                // ⚠️注意:CHM 保证的是线程安全,但 map.get + map.put 这个组合操作本身不是原子的!
                // 所以在真实业务中,我们通常用 atomic 操作或者 compute 方法
                // 但为了演示最基础的“不丢数据”能力,咱们这里稍微加个小锁或者利用 CHM 特性
                // 咱们这里直接演示最粗暴的覆盖问题,看看谁能活下来
                synchronized (map) { 
                    // 这里为了模拟单纯的数据插入完整性,稍微偷个懒加了 synchronized
                    // 但如果你去掉 synchronized,HashMap 会丢数据或者抛异常,CHM 虽然如果不加锁逻辑上会有覆盖,
                    // 但内部结构绝对不会坏!
                    // 实际上 CHM 推荐用法是 map.merge() 或 map.compute()
                    map.put("key", map.getOrDefault("key", 0) + 1);
                }
                latch.countDown();
            });
        }
        
        latch.await();
        executor.shutdown();
        long end = System.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. 多维度对比总结:一张表看懂江湖地位 📊

咱把这几个哥们儿拉在一起,做一个全方位的“相亲角”对比,方便大家记忆:

特性 HashMap Hashtable ConcurrentHashMap (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 的笨重。相信我,那一刻,你就是全场最靓的仔!✨

还在用 Hashtable 抗并发?面试官问你 ConcurrentHashMap 底层原理,你真的能答到底裤都不剩吗?

… …

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

… …

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!还在用 Hashtable 抗并发?面试官问你 ConcurrentHashMap 底层原理,你真的能答到底裤都不剩吗?

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。