文章目录
- 1. 并发安全的演进:从分段锁到CAS
- JDK 1.7的分段锁设计
- JDK 1.8的革命性改进
- 2. 核心技术机制深度剖析
- 2.1 CAS(Compare And Swap)操作
- 2.2 synchronized精细化同步
- 2.3 volatile变量的魔法
- 3. 关键操作的线程安全实现
- 3.1 put操作—如何安全地插入数据
- 3.2 get操作—无锁读取的高效设计
- 3.3 size操作—智能的统计策略
- 4. 实战场景与最佳实践
- 4.1 缓存场景下的应用
- 4.2 计数器场景的优化
- 4.3 需要注意的陷阱
- 5. 性能对比与优化建议
- 5.1 与其它并发容器的对比
- 5.2 优化建议
- 6. 总结与展望
- 参考文章
大家好,我是你们的船长:科威舟,今天给大家分享一下ConcurrentHashMap如何优雅地实现线程安全?
在多线程编程的世界里,数据竞争和线程安全是每个程序员必须面对的挑战。今天,我们要揭秘的是Java并发包中一个真正的明星—ConcurrentHashMap,它如何在不牺牲性能的情况下保证线程安全。
在开始深入探讨之前,先让我们想象一个场景:一个大型超市(我们的程序)有多个收银台(线程),顾客(数据)需要快速结账(处理)。
如果所有顾客都排在一个收银台,效率肯定低下—这就是HashTable的做法;如果完全不管排队秩序,让顾客随意争抢—这是HashMap的线程不安全做法。
而ConcurrentHashMap则像是一个智能的超市管理系统,它既保证了秩序,又最大化提高了效率。
1. 并发安全的演进:从分段锁到CAS
JDK 1.7的分段锁设计
在JDK 1.7中,ConcurrentHashMap采用了一种称为"分段锁"的创新设计。它将整个哈希表分成多个段(Segment),每个段都是一个独立的哈希表,拥有自己的锁。
这相当于将一个大超市划分成多个部门(食品区、服装区、家电区),每个部门有自己独立的收银台。不同部门的顾客可以同时结账,只有同一部门的顾客才需要排队。
// JDK 1.7中的Segment类就是一个独立的哈希表加锁staticfinalclassSegment<K,V>extendsReentrantLockimplementsSerializable{transientvolatileHashEntry<K,V>[]table;transientintcount;// ...}具体put操作如下:
publicVput(Kkey,Vvalue){inthash=hash(key);intsegmentIndex=getSegmentIndex(hash);// 定位到哪个Segmentreturnsegments[segmentIndex].put(key,hash,value,false);}这种设计显著减少了锁的竞争,默认情况下有16个段,意味着理论上允许16个线程并发写入,相比HashTable的全表锁,性能提升巨大。
JDK 1.8的革命性改进
JDK 1.8对ConcurrentHashMap进行了彻底重写,放弃了分段锁,采用了更为精细的CAS + synchronized方案。
这好比将超市的收银系统进一步优化—现在每个商品货架都有了自己的微型管理系统,只有在真正需要时才进行同步。
// JDK 1.8的putVal方法关键部分finalVputVal(Kkey,Vvalue,booleanonlyIfAbsent){// ...if((f=tabAt(tab,i=(n-1)&hash))==null){// 如果位置为空,使用CAS无锁插入if(casTabAt(tab,i,null,newNode<K,V>(hash,key,value,null)))break;}else{synchronized(f){// 只锁住当前桶的第一个节点// 具体的插入逻辑}}// ...}2. 核心技术机制深度剖析
2.1 CAS(Compare And Swap)操作
CAS是一种乐观锁机制,它包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。当且仅当V的值等于A时,CAS才会通过原子方式用B更新V的值,否则什么都不做。
在ConcurrentHashMap中,大量使用了CAS操作:
// 获取tab数组的第i个节点staticfinal<K,V>Node<K,V>tabAt(Node<K,V>[]tab,inti){return(Node<K,V>)U.getObjectVolatile(tab,((long)i<<ASHIFT)+ABASE);}// 使用CAS算法设置i位置上的节点staticfinal<K,V>booleancasTabAt(Node<K,V>[]tab,inti,Node<K,V>c,Node<K,V>v){returnU.compareAndSwapObject(tab,((long)i<<ASHIFT)+ABASE,c,v);}这就像去超市购物时,看到心仪商品标签上写着"限购1件",你拿了一件去结账。如果收银系统发现库存还有,就允许购买;如果已经被别人买走,你就需要重新选择。
2.2 synchronized精细化同步
虽然CAS很高效,但并非适用于所有场景。JDK 1.8在发生哈希冲突时,使用synchronized对单个桶(桶的第一个节点)进行加锁。
锁粒度从分段缩小到单个桶,这是性能提升的关键。现代JDK对synchronized做了大量优化,性能损失已大大降低。
2.3 volatile变量的魔法
ConcurrentHashMap中大量使用volatile关键字来保证内存可见性:
staticclassNode<K,V>implementsMap.Entry<K,V>{finalinthash;finalKkey;volatileVvalue;// 使用volatile保证可见性volatileNode<K,V>next;// ...}volatile相当于给变量加了一个"广播系统"—任何线程的修改都会立即通知到所有其他线程,保证了数据的实时可见性。
3. 关键操作的线程安全实现
3.1 put操作—如何安全地插入数据
put操作是ConcurrentHashMap最复杂的方法,其线程安全通过以下步骤保证:
- 计算哈希值:根据key计算hash,确定桶的位置
- 表未初始化则先初始化:使用sizeCtl变量控制,保证只初始化一次
- 桶为空则CAS插入:如果定位到的桶为空,直接CAS插入新节点
- 桶不为空则synchronized加锁:锁住桶的第一个节点,处理链表或红黑树插入
- 判断是否需要扩容:在达到阈值时进行线程安全的扩容
整个过程就像去图书馆还书:先找到正确的书架(哈希定位),如果书架空着直接放书(CAS插入);如果书架上已有书,则需要管理员的协助(synchronized加锁)来整理。
3.2 get操作—无锁读取的高效设计
get操作是完全无锁的,这也是ConcurrentHashMap在高并发读场景下性能优异的关键。
publicVget(Objectkey){Node<K,V>[]tab;Node<K,V>e,p;intn,eh;Kek;inth=spread(key.hashCode());if((tab=table)!=null&&(n=tab.length)>0&&(e=tabAt(tab,(n-1)&h))!=null){// 无锁遍历链表或红黑树if((eh=e.hash)==h){if((ek=e.key)==key||(ek!=null&&key.equals(ek)))returne.val;}// ... 其他情况处理}returnnull;}这就像超市的顾客可以随意浏览商品(读取数据)而不用打扰收银系统,只有当他们要实际购买(修改数据)时才需要排队。
3.3 size操作—智能的统计策略
size操作的实现体现了ConcurrentHashMap在精确性和性能之间的平衡:
- 先尝试无锁统计,遍历所有节点计数
- 如果检测到有并发修改,则重试
- 如果重试多次仍然有修改,则转为加锁统计
这种设计类似于超市人流量统计:先通过摄像头大致估算(无锁统计),如果人流量变化太频繁,再派人实际点数(加锁统计)。
4. 实战场景与最佳实践
4.1 缓存场景下的应用
ConcurrentHashMap是实现高性能缓存的理想选择。例如,我们可以实现一个带过期时间的缓存:
publicclassExpiringCache<K,V>{privatefinalConcurrentHashMap<K,CacheValue<V>>cache=newConcurrentHashMap<>();publicvoidput(Kkey,Vvalue,longttl){CacheValue<V>cacheValue=newCacheValue<>(value,System.currentTimeMillis()+ttl);cache.put(key,cacheValue);}publicVget(Kkey){CacheValue<V>cacheValue=cache.get(key);if(cacheValue==null||cacheValue.isExpired()){cache.remove(key);returnnull;}returncacheValue.getValue();}privatestaticclassCacheValue<V>{privatefinalVvalue;privatefinallongexpiryTime;// 构造方法和getter省略booleanisExpired(){returnSystem.currentTimeMillis()>expiryTime;}}}4.2 计数器场景的优化
对于高并发计数器,ConcurrentHashMap提供了更好的解决方案:
// 线程安全的计数器ConcurrentHashMap<String,Long>counter=newConcurrentHashMap<>();// 原子性增加计数publiclongincrement(Stringkey){returncounter.compute(key,(k,v)->v==null?1L:v+1L);}// 获取所有计数和publiclongtotalCount(){returncounter.reduceValuesToLong(1,v->v,0L,Long::sum);}4.3 需要注意的陷阱
虽然ConcurrentHashMap很强大,但使用时仍需注意:
复合操作不是原子性的:多个连续操作需要外部同步
// 不安全的复合操作if(!map.containsKey(key)){map.put(key,value);// 这两个操作之间可能有其他线程修改}// 安全的原子操作map.putIfAbsent(key,value);迭代器的弱一致性:ConcurrentHashMap的迭代器反映的是创建时的状态,不抛出ConcurrentModificationException
null值禁止:与HashMap不同,ConcurrentHashMap不允许key和value为null,避免在并发环境中的歧义
5. 性能对比与优化建议
5.1 与其它并发容器的对比
| 容器 | 锁粒度 | 读性能 | 写性能 | 适用场景 |
|---|---|---|---|---|
| Hashtable | 全表锁 | 差 | 差 | 不推荐使用 |
| Collections.synchronizedMap | 全表锁 | 差 | 差 | 低并发场景 |
| ConcurrentHashMap(JDK 1.7) | 段锁 | 良好 | 良好 | 中等并发 |
| ConcurrentHashMap(JDK 1.8) | 桶锁 | 优秀 | 优秀 | 高并发 |
5.2 优化建议
合理设置初始容量:避免频繁扩容,根据预估数据量设置合适的初始大小
并发级别设置:在JDK 1.7中,可以根据并发线程数设置合适的并发级别;在JDK 1.8中,这个参数主要是为了兼容性
考虑键的哈希质量:键对象的哈希码分布影响性能,尽量避免哈希冲突
6. 总结与展望
ConcurrentHashMap是Java并发编程中的一颗明珠,它通过精细的锁设计、CAS操作和无锁读技术,在保证线程安全的同时提供了优异的性能。
从JDK 1.7的分段锁到JDK 1.8的CAS+synchronized,体现了并发优化技术的演进趋势:锁粒度越来越细,无锁化范围越来越大。
随着虚拟线程在Java 19中的引入,ConcurrentHashMap可能会有新的优化方向,比如更好地应对海量线程访问的场景。但它的核心思想—尽可能减少锁竞争,最大化并发度—将一直是高并发编程的指导原则。
正如ConcurrentHashMap的作者Doug Lea所说:"好的并发设计就像是精心设计的交通系统,既要保证车辆有序通行,又要避免不必要的等待。"ConcurrentHashMap正是这一理念的完美体现。
参考文章
- https://juejin.cn/post/7250037058684518459
- https://bbs.huaweicloud.com/blogs/451772
- https://www.cnblogs.com/i-xq/p/13073727.html
- https://blog.csdn.net/Cactus_Lrg/article/details/82781034
- https://blog.csdn.net/weixin/43207025/article/details/114855495
- https://blog.csdn.net/weixin/45187434/article/details/127374646
- https://blog.csdn.net/qq_43279073/article/details/97171662
- https://blog.csdn.net/chi_666/article/details/145955885
- https://blog.csdn.net/qq_38129621/article/details/147375839
- https://www.cnblogs.com/chougoushi/p/14498903.html
本文旨在用通俗易懂的方式讲解ConcurrentHashMap的线程安全机制,实际源码更为复杂,建议读者结合JDK源码深入学习。如有错误欢迎指正!
更多技术干货欢迎关注微信公众号科威舟的AI笔记~
【转载须知】:转载请注明原文出处及作者信息