news 2026/6/9 18:32:47

多线程下用 ConcurrentHashMap,到底要不要加 volatile?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多线程下用 ConcurrentHashMap,到底要不要加 volatile?
  • 多线程下用 ConcurrentHashMap,到底要不要加 volatile?
    • 先搞懂两个关键角色
      • ConcurrentHashMap 是做什么的
      • volatile 又是做什么的
    • 分场景看,到底要不要加 volatile
      • 场景一:ConcurrentHashMap 引用不会改变,不需要加 volatile
      • 场景二:ConcurrentHashMap 引用会被重新赋值,必须加 volatile
    • 结合实际业务,再加深理解
    • 再延伸一个容易忽略的点

多线程下用 ConcurrentHashMap,到底要不要加 volatile?

这段时间在看并发相关的面试题,碰到一个特别容易让人绕晕的问题:多线程环境里使用 ConcurrentHashMap,要不要把它声明成 volatile 才能保证线程安全?

单独拎出来 ConcurrentHashMap 和 volatile,每个知识点我都能说上几句,可把它们放在一起提问,瞬间就有种熟悉又陌生的感觉,琢磨了好一会儿才理清楚里面的逻辑,今天就把自己的思考过程整理出来,都是很实在的理解,没有什么官方套话。

先把两个核心概念掰扯明白,这是搞懂整个问题的基础,后续的分析都要围绕这两个点展开。

先搞懂两个关键角色

ConcurrentHashMap 是做什么的

日常开发里,ConcurrentHashMap 算是并发场景的常客,面试里也总爱把它和 HashMap 放在一起对比。大家都知道 HashMap 不支持多线程并发操作,在多线程环境下会出现数据错乱的问题,而 ConcurrentHashMap 就是 Java 提供的线程安全的哈希表实现。

但这里必须抓住一个核心点:ConcurrentHashMap 的线程安全,只局限在它自身方法内部的操作
比如调用它的 put、get、remove 这些方法,多个线程同时执行,底层通过 CAS 加同步机制等方式,能保证单个方法执行的原子性和数据一致性,不会出现并发修改导致的异常。但它管不了的是,这个 ConcurrentHashMap 实例的引用,在多线程之间的可见性问题。

volatile 又是做什么的

volatile 也是并发编程里的高频关键字,它的作用其实很明确,主要解决两个问题:一是保证变量的可见性,一个线程修改了被 volatile 修饰的变量,其他线程能立刻读取到最新的值,不会出现线程本地缓存和主内存数据不一致的情况;二是禁止指令重排序,避免编译器和处理器对指令的执行顺序做优化,导致多线程下出现意料之外的问题。

这里要划一个重点:volatile 修饰的是变量,也就是对象的引用,而不是对象内部的数据。想把 ConcurrentHashMap 和 volatile 关联起来,前提是 ConcurrentHashMap 作为一个引用变量,存在被修改的可能,否则讨论 volatile 就没有任何意义。

分场景看,到底要不要加 volatile

这个问题根本没有绝对的“要”或“不要”,必须结合实际的代码场景来判断,两种情况的区别非常明显。

场景一:ConcurrentHashMap 引用不会改变,不需要加 volatile

当我们在代码中,初始化 ConcurrentHashMap 之后,全程只调用它的内部方法操作数据,从来不会重新给这个变量赋值,让它指向新的实例,这种情况下完全不需要加 volatile。

最典型的写法就是用final修饰,直接锁定引用:

publicclassCacheService{// 用 final 保证引用不可变,全程只会操作这一个 CHM 实例privatestaticfinalConcurrentHashMap<String,Object>concurrentCache=newConcurrentHashMap<>();publicvoidputData(Stringkey,Objectvalue){// 仅调用 CHM 自身的方法,内部已保证线程安全concurrentCache.put(key,value);}publicObjectgetData(Stringkey){returnconcurrentCache.get(key);}}

在这段代码里,concurrentCache的引用从初始化后就不会再改变,所有线程操作的都是同一个 ConcurrentHashMap 实例。此时线程安全完全由 ConcurrentHashMap 自身的方法保证,volatile 在这里没有任何发挥的空间,加上反而属于多余的代码。

场景二:ConcurrentHashMap 引用会被重新赋值,必须加 volatile

如果业务逻辑中,需要替换掉原来的 ConcurrentHashMap 实例,把新的实例赋值给同一个变量,这时候就必须使用 volatile 来保证引用的可见性。

比如常见的缓存全量更新场景,代码大概是这样:

publicclassCacheService{// 引用可能被替换,必须加 volatile 保证可见性privatevolatileConcurrentHashMap<String,Object>concurrentCache=newConcurrentHashMap<>();/** * 全量更新缓存,直接替换整个 CHM 实例 */publicvoidrefreshCache(){// 创建新的缓存实例,加载全量数据ConcurrentHashMap<String,Object>newCache=newConcurrentHashMap<>();// 模拟加载缓存数据的逻辑newCache.put("user:1","张三");newCache.put("user:2","李四");// 替换原有的缓存引用concurrentCache=newCache;}publicObjectgetData(Stringkey){returnconcurrentCache.get(key);}}

在这个场景里,concurrentCache这个引用变量会被重新赋值,指向新的 ConcurrentHashMap 实例。如果不加 volatile,当一个线程执行了refreshCache方法替换了引用后,其他线程可能还在读取旧的引用,使用的是过时的缓存数据,这就产生了线程安全问题。
而加上 volatile 之后,就能保证引用修改的可见性,所有线程都能立即获取到最新的实例引用,再结合 ConcurrentHashMap 自身的方法安全,整个流程才是完整的线程安全。

结合实际业务,再加深理解

平时做 Spring Web 开发的时候,经常会把 ConcurrentHashMap 作为成员变量放在 Controller 里,这里就很容易踩坑,我们可以看一段实际的示例代码:

@RestControllerpublicclassDataController{// 单例 Bean 下的 CHM 成员变量privateConcurrentHashMap<String,String>dataMap=newConcurrentHashMap<>();@GetMapping("/add")publicStringaddData(Stringkey,Stringvalue){dataMap.put(key,value);return"添加成功";}@GetMapping("/get")publicStringgetData(Stringkey){returndataMap.get(key);}/** * 新增的方法,直接替换 CHM 引用 */@GetMapping("/reset")publicStringresetData(){// 此处直接重新赋值,修改了引用dataMap=newConcurrentHashMap<>();return"缓存已重置";}}

Spring 的 Controller 默认是单例作用域,所有的请求都会共享同一个 DataController 实例,也就共享同一个dataMap变量。
在只调用addDatagetData方法时,dataMap的引用没有改变,依靠 ConcurrentHashMap 自身的安全性,不会出现线程问题。但新增了resetData方法后,dataMap会被重新赋值,指向新的实例,此时没有 volatile 修饰,就会出现部分线程读取到旧实例、部分读取到新实例的问题,导致数据不一致。

解决这个问题的方式也很清晰:

  1. dataMap加上volatile关键字,保证引用的可见性;
  2. dataMap加上final关键字,禁止引用被重新赋值,从根源上杜绝问题;
  3. 将 Controller 的作用域改为 prototype,每次请求创建新实例,让每个线程操作独立的 CHM,但这种方式会增加内存开销,需要结合业务权衡。

再延伸一个容易忽略的点

这里还要补充一个很重要的误区,就算我们用了线程安全的 ConcurrentHashMap,也不代表所有场景下都绝对安全,尤其是涉及到复合操作的时候。

举个简单的例子,想要实现“如果 key 不存在,就放入数据”的逻辑:

publicvoidputIfNotExist(Stringkey,Stringvalue){// 先查询,再插入,两步操作if(!concurrentCache.containsKey(key)){concurrentCache.put(key,value);}}

ConcurrentHashMap 的containsKeyput方法都是线程安全的,但这两个方法组合在一起,就变成了非原子操作。多线程环境下,可能两个线程同时判断 key 不存在,然后先后执行 put 方法,导致后执行的线程覆盖了先执行的线程的数据。

这种情况,ConcurrentHashMap 自身的线程安全解决不了,需要我们额外处理,比如使用 ConcurrentHashMap 提供的原子方法putIfAbsent,或者通过加锁来保证复合操作的原子性。

这也印证了一个道理:线程安全是一个全局的问题,不能只依赖某一个组件的特性,就觉得万事大吉,所有的逻辑都要结合具体的使用场景去分析。

回到最开始的问题,现在再看,答案其实已经很清晰了。ConcurrentHashMap 负责自身方法的线程安全,volatile 负责引用变量的可见性,两者的作用颗粒度完全不同。只有当 ConcurrentHashMap 的引用存在被修改的场景时,才需要使用 volatile,否则完全没有必要添加。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 14:53:34

工业控制界面异常状态的多维测试策略与实践指南

一、ICS测试的特殊性与核心挑战 工业控制系统深度融合物理设备与信息网络&#xff0c;其异常检测需兼顾实时响应、设备安全与数据完整性。传统软件测试方法在以下场景存在局限&#xff1a; 攻击面扩大&#xff1a;APT攻击利用0-day漏洞穿透常规检测 状态复杂性&#xff1a;设…

作者头像 李华
网站建设 2026/5/22 7:19:12

Java springboot基于微信小程序的宠物医院系统挂号就诊服务预约(源码+文档+运行视频+讲解视频)

文章目录 系列文章目录目的前言一、详细视频演示二、项目部分实现截图三、技术栈 后端框架springboot前端框架vue持久层框架MyBaitsPlus微信小程序介绍系统测试 四、代码参考 源码获取 目的 基于微信小程序的宠物医院系统&#xff0c;采用Spring Boot框架与微信小程序技术&am…

作者头像 李华
网站建设 2026/6/10 14:15:55

稳定性质量系列-高可用领域自动化保障体系建设方案二

高可用自动化保障体系建设&#xff0c;核心是“放弃大而全、聚焦核心痛点”&#xff0c;以“低成本、快落地、可复用”为原则&#xff0c;精简平台建设环节&#xff0c;优先落地“故障早发现、常见故障快处置、上线少出问题”三大核心能力&#xff0c;弱化复杂智能模块&#xf…

作者头像 李华
网站建设 2026/6/10 14:21:22

AI时代的分工重构:开发者与测试员的协同进化

被误读的“取代论” 近年来&#xff0c;“AI将取代测试工程师”的论调甚嚣尘上&#xff0c;尤其伴随全栈工程师能力的泛化&#xff0c;开发者似乎具备了“自测自验”的技术基础。然而&#xff0c;行业数据与实战经验表明&#xff1a;AI并未消灭测试岗位&#xff0c;而是重构其…

作者头像 李华
网站建设 2026/6/10 14:22:42

互联网大厂Java面试:从消息队列到微服务架构场景解析

互联网大厂Java面试&#xff1a;从消息队列到微服务架构场景解析 故事背景 在一家知名互联网大厂的技术面试现场&#xff0c;面试官李云龙严肃地坐在桌前&#xff0c;面试的是一个略显搞笑的程序员谢宝庆。这次面试围绕消息队列与微服务架构展开&#xff0c;场景设定为广告与营…

作者头像 李华