ConcurrentHashMap线程安全原理解析
本文深入剖析了Java并发集合的核心代表——ConcurrentHashMap的底层原理与设计智慧,揭示其为何在高并发场景下远超HashTable和Collections.synchronizedMap:通过JDK 7的分段锁到JDK 8的CAS+synchronized精细化协同,实现无锁读、桶级细粒度写、链表/红黑树动态优化及多线程协作扩容,让并发性能跃升;同时对比介绍了CopyOnWriteArrayList、ConcurrentLinkedQueue等并发集合的适用边界,强调在真实业务中应优先选用java.util.concurrent包中为并发而生的高性能原生组件——读懂它,就是掌握Java高并发编程的关键突破口。
在Java中,实现线程安全集合的首选方式是使用java.util.concurrent包中的并发集合类。1. ConcurrentHashMap通过分段锁(JDK 7及之前)或CAS+synchronized(JDK 8及以上)机制提供高并发性能,避免了全局锁带来的性能瓶颈;2. CopyOnWriteArrayList适用于读多写少场景,通过写时复制保证线程安全;3. ConcurrentLinkedQueue和ArrayBlockingQueue分别适用于无界非阻塞和有界阻塞队列需求。相较于HashTable和Collections.synchronizedMap的粗粒度锁机制,ConcurrentHashMap通过细粒度锁和无锁读操作显著提升了并发效率。其get方法利用volatile变量和不可变Node对象实现无需加锁的线程安全读取。put操作则结合CAS尝试无锁插入,并在冲突时锁定单个桶节点以支持并发修改。扩容时采用多线程协作迁移数据的方式,进一步减少了对整体性能的影响。因此,在并发编程中应优先选用这些专为并发优化的集合类。

在Java中,要实现多线程安全的集合,最直接且高效的方式是使用java.util.concurrent包下提供的并发集合类。这些类经过精心设计,能够在高并发场景下提供优秀的性能和线程安全性,其中ConcurrentHashMap就是典范。当然,你也可以通过Collections.synchronizedMap()等方式对现有集合进行包装,但其并发性能通常不如专门的并发集合。

解决方案
当我们需要一个线程安全的Map时,ConcurrentHashMap是首选。它通过精细的锁控制(而非全局锁)实现了高并发访问。对于列表,CopyOnWriteArrayList在读多写少场景下表现出色,因为它在写入时会复制底层数组,保证了读操作的无锁化。而对于队列,ConcurrentLinkedQueue和ArrayBlockingQueue等则提供了不同的并发队列实现,前者是无界非阻塞的,后者是有界阻塞的。选择哪种取决于具体的业务需求和并发模式。通常,优先考虑java.util.concurrent包中的类,它们在设计上就考虑到了并发问题,且性能经过了高度优化。
ConcurrentHashMap为什么比HashTable和Collections.synchronizedMap更高效?
这其实是个很有趣的问题,它揭示了并发编程中锁粒度对性能的决定性影响。HashTable和通过Collections.synchronizedMap包装的HashMap,它们实现线程安全的方式是简单粗暴的:在几乎所有公共方法上都加上了synchronized关键字。这意味着,无论你是在读数据还是写数据,只要有一个线程在操作这个Map,整个Map就被锁住了,其他所有试图访问的线程都得排队等待。这就像一家餐厅,只有一个服务员,每次只能服务一位顾客,效率自然低下。

ConcurrentHashMap则完全不同。在JDK 7及之前的版本,它采用了“分段锁”(Segment Lock)的机制,将整个Map划分为若干个Segment,每个Segment都是一个独立的HashTable。当一个线程修改某个Segment时,只会锁住这个Segment,其他线程仍然可以访问其他Segment。这就大大提升了并发度。
而到了JDK 8,ConcurrentHashMap的实现进一步优化,取消了Segment的概念,转而采用了“CAS(Compare-And-Swap)+ synchronized”的策略。它的核心思想是:

- 无锁读操作:
get操作通常不需要加锁,因为它利用了volatile和内存屏障的特性,保证了读取到的数据是最新的。 - CAS操作: 对于一些简单的修改操作,例如在桶(bucket)为空时尝试放入第一个节点,
ConcurrentHashMap会尝试使用CAS操作来原子性地更新。如果成功,则无需加锁。 - 细粒度锁: 只有当发生哈希冲突,需要修改链表或红黑树结构时,才会对链表或红黑树的头节点进行
synchronized锁定。这意味着,即使在同一个桶内,只要不是修改同一个链表或树的结构,不同的线程也可以同时进行操作。当链表过长时,还会转换为红黑树以优化查找性能。
这种设计使得ConcurrentHashMap在大多数情况下都能实现很高的并发度,因为大部分操作都不需要等待全局锁,甚至不需要等待桶级别的锁,只有在真正需要修改共享结构时才加锁,而且锁的粒度非常小。
ConcurrentHashMap的get方法是如何实现线程安全的?
ConcurrentHashMap的get方法实现线程安全,但又无需加锁,这得益于Java内存模型(JMM)中的volatile关键字和其内部数据结构的巧妙设计。
ConcurrentHashMap的底层是一个Node数组,这个数组被volatile修饰。volatile确保了两点:
- 可见性: 当一个线程修改了
Node数组中的某个元素(比如替换了一个Node对象),这个修改会立即对所有其他线程可见。 - 有序性: 编译器和处理器不会对
volatile变量的操作进行重排序,保证了操作的顺序性。
在get操作中:
- 它首先会读取
volatile修饰的table数组,确保获取到的是最新的数组引用。 - 接着,根据键的哈希值定位到对应的桶(
Node数组的索引)。 - 然后,它遍历该桶中的链表或红黑树来查找目标键值对。
这里的关键在于,Node对象本身(存储键值对)一旦被创建并放入桶中,其内部的键和值通常是不可变的(final修饰)。即使需要更新某个键的值,ConcurrentHashMap也可能通过创建新的Node并替换旧Node的方式来实现,或者利用CAS操作原子性地更新Node内部的值。由于get操作只是读取已经存在的Node及其内容,而volatile保证了Node数组的可见性,使得get总是能看到最新的Node引用。在遍历链表或红黑树时,由于Node之间的引用也是通过volatile或CAS保证了可见性和原子性,因此get操作可以在没有显式锁的情况下安全地进行。它避免了读写冲突,因为读操作不会阻塞写操作,写操作也不会阻塞读操作(除非写操作正在改变整个桶的结构,比如链表转红黑树,但即便如此,get也只是读取旧的引用,最终会看到最新的状态)。
ConcurrentHashMap在put操作中如何处理并发冲突和扩容?
ConcurrentHashMap的put操作是其并发性能的核心体现,它巧妙地结合了CAS和synchronized来处理并发冲突和扩容。
处理并发冲突:
当一个线程调用put方法时:
- 计算哈希值: 首先根据键计算哈希值,并确定目标桶(
Node数组的索引)。 - 检查桶状态:
- 如果目标桶为空,
put操作会尝试使用CAS来放置第一个Node。如果CAS成功,操作完成,无需加锁。 - 如果目标桶不为空,说明该桶已经有元素。此时,
put操作会尝试对该桶的头节点进行synchronized锁定。这个锁是针对单个桶的,而不是整个Map。
- 如果目标桶为空,
- 遍历与插入/更新:
- 在获取到桶锁后,线程会遍历链表或红黑树。
- 如果找到相同的键,则更新其值。这可能涉及
CAS操作(如果Node支持原子更新)或在锁内直接修改。 - 如果未找到相同键,则将新的
Node插入到链表末尾或红黑树中。
- 链表转红黑树: 如果某个桶的链表长度超过了
TREEIFY_THRESHOLD(默认为8),ConcurrentHashMap会将该链表转换为红黑树,以优化查找性能。这个转换过程也是在桶锁的保护下进行的。
通过这种方式,ConcurrentHashMap避免了对整个Map的锁定,允许多个线程同时修改不同桶中的元素,大大提高了并发度。只有当多个线程恰好要修改同一个桶时,才需要竞争同一个桶锁。
处理扩容(Resizing):ConcurrentHashMap的扩容机制也设计得非常巧妙,支持并发扩容:
- 触发扩容: 当Map中的元素数量达到一定阈值(
capacity * loadFactor)时,会触发扩容。 - 创建新表:
ConcurrentHashMap会创建一个两倍于当前容量的新Node数组。 - 并发迁移: 扩容并不是由一个线程独自完成的,而是可以由多个线程协助完成。
- 当一个线程发现Map正在扩容时(通过检查桶中是否有
ForwardingNode标记),它会加入到迁移工作中。 - 每个线程负责迁移一部分桶。它会从旧表的末尾开始,向头部遍历,每次处理一个或多个桶。
- 在迁移每个桶时,该线程会锁定当前桶的头节点,然后将该桶中的所有
Node重新哈希并复制到新表中对应的位置。 - 迁移完成后,旧表中的对应桶会被放置一个
ForwardingNode,表示该桶的数据已经迁移到新表,并指向新表。
- 当一个线程发现Map正在扩容时(通过检查桶中是否有
- CAS更新表引用: 当所有桶都迁移完成后,
ConcurrentHashMap会使用CAS操作原子性地将table引用指向新的Node数组。
这种并发迁移的设计,使得扩容过程不会长时间阻塞整个Map的读写操作,进一步提升了ConcurrentHashMap在高并发场景下的可用性和性能。它巧妙地利用了CAS和细粒度锁,将一个看似复杂的全局操作分解为多个可以并行执行的小任务。
以上就是本文的全部内容了,是否有顺利帮助你解决问题?若是能给你带来学习上的帮助,请大家多多支持golang学习网!更多关于文章的相关知识,也可关注golang学习网公众号。
外部类可见性与_default权限解析
- 上一篇
- 外部类可见性与_default权限解析
- 下一篇
- 文心一言官网入口地址及登录方法
-
- 文章 · java教程 | 14小时前 | Java · 异步编程 · 后端开发 · CompletableFuture · 接口聚合 · java 结果合并 completablefuture 并行调用 超时兜底
- Java CompletableFuture 多接口聚合完整流程:并行调用、超时兜底和结果合并
- 428浏览 收藏
-
- 文章 · java教程 | 16小时前 | Java · 线程安全 · DateTimeFormatter · 日期处理 · 并发问题 · java 线程安全 日期格式化 threadlocal SimpleDateFormat DateTimeFormatter
- Java SimpleDateFormat 日期偶发错乱怎么办:从共享实例到线程安全一步步排查
- 481浏览 收藏
-
- 文章 · java教程 | 2天前 | http接口 · httpclient · Java教程 · 接口调试 · 超时处理 · java 接口调用 httpclient 超时控制 状态码 响应体
- Java HttpClient 调接口实战:超时、状态码和响应体这样处理
- 224浏览 收藏
-
- 文章 · java教程 | 2天前 | 时间处理 · instant · Java教程 · 时区转换 · DateTimeFormatter · java DateTimeFormatter java.time 时区处理 ZoneId INSTANT
- Java 时间与时区处理实战:Instant、ZoneId 和 DateTimeFormatter 怎么配
- 461浏览 收藏
-
- 文章 · java教程 | 2天前 | Java · Stream · 集合统计 · 分组聚合 · Collectors · java Stream Collectors groupingBy counting summarizingInt
- Java Stream 分组统计实战:groupingBy、counting 和 summarizingInt 怎么用
- 478浏览 收藏
-
- 文章 · java教程 | 2天前 | Java · 文件读取 · 异常处理 · 资源管理 · try-with-resources · java 异常处理 try-with-resources 资源关闭 AutoCloseable 文件流
- Java try-with-resources 资源关闭实战:文件流和目录扫描这样写更稳
- 268浏览 收藏
-
- 文章 · java教程 | 3天前 | Java教程 · 后端开发 · BigDecimal · 金额计算 · java 舍入 bigdecimal 浮点误差 金额计算 RoundingMode
- Java BigDecimal 金额计算实战:避免浮点误差和舍入问题
- 324浏览 收藏
-
- 文章 · java教程 | 3天前 | 异步编程 · Java教程 · 超时治理 · CompletableFuture · java 异步任务 超时处理 completablefuture orTimeout completeOnTimeout
- Java CompletableFuture 超时处理实战:orTimeout 和兜底结果怎么选
- 421浏览 收藏
-
- 文章 · java教程 | 1星期前 | 并发编程 · 生产实践 · Java教程 · JDK25 · 虚拟线程 · 虚拟线程 Java 25 JEP 505 Structured Concurrency StructuredTaskScope
- Java 25 Structured Concurrency 实战:别让 CompletableFuture 把超时拖散
- 443浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- 剧云
- 剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
- 112次使用
-
- 万象有声
- 万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
- 115次使用
-
- Red Skill
- 小红书创作服务平台为小红书创作者和机构提供视频上传、数据分析、粉丝管理、创作指导等多项运营服务,助力用户解锁更多创作者专属功能,体验高效创作!
- 115次使用
-
- MiMo Code
- MiMo Code 是小米大模型团队开源的新一代 AI 编程助手,面向开发者提供代码理解、生成与辅助开发能力,适合作为 AI 编程工具收藏和体验。
- 219次使用
-
- TRAE Work
- TRAE AI IDE | 国内首款 AI 原生集成开发环境,深度集成 Doubao-1.5-pro 与 DeepSeek 模型,支持中文自然语言一键生成完整代码框架,实时预览前端效果并智能修复 BUG。首创 Builder 模式实现需求到代码的自动化开发,兼容 Windows/macOS 系统,官网下载即用。
- 249次使用
-
- 提升Java功能开发效率的有力工具:微服务架构
- 2023-10-06 501浏览
-
- 掌握Java海康SDK二次开发的必备技巧
- 2023-10-01 501浏览
-
- 如何使用java实现桶排序算法
- 2023-10-03 501浏览
-
- Java开发实战经验:如何优化开发逻辑
- 2023-10-31 501浏览
-
- 如何使用Java中的Math.max()方法比较两个数的大小?
- 2023-11-18 501浏览

