前言
各位码友们,作为一名后端开发,面试时是不是经常被问到并发编程的那些事儿?今天咱们就来聊聊那个让无数程序员又爱又恨的ConcurrentHashMap ,以及它的好基友CAS。准备好瓜子饮料,咱们开车啦!
一、什么是CAS?比相亲还谨慎的原子操作
1.1 CAS的自我介绍
CAS(Compare And Swap),翻译过来就是"比较并交换",是一种无锁编程的原子操作。它的行为模式像极了相亲:
java
// 伪代码版相亲过程
public boolean 相亲(我, 心仪对象, 新对象) {
if (当前状态 == 心仪对象) {
当前状态 = 新对象; // 牵手成功!
return true;
} else {
return false; // 抱歉,你来晚了
}
}
在Java中,CAS是通过sun.misc.Unsafe
类实现的,但咱们日常开发中更多使用的是它的包装类:
java
// Java中的CAS实战
AtomicInteger atomicInt = new AtomicInteger(0);
// 如果当前值是0,就把它改成1
boolean success = atomicInt.compareAndSet(0, 1);
System.out.println("操作结果:" + success); // 输出:true
1.2 CAS的底层原理
CAS的底层其实是CPU的原子指令(cmpxchg),这个过程是不可中断的,就像:
"我看看现在是不是A,如果是的话我就换成B,这期间谁都不能打扰我!"
二、ABA问题:你的前女友改头换面回来了
2.1 什么是ABA问题?
想象一下这个悲伤的故事:
- 你看到女朋友在家(值是A)
- 你去买奶茶的功夫,她出门做了个发型又回来了(A→B→A)
- 你以为她一直在家,其实...
java
// ABA问题示例
public class ABADemo {
private static AtomicReference<String> gf = new AtomicReference<>("小美");
public static void main(String[] args) {
// 线程1:检查女朋友是否还是小美
new Thread(() -> {
String expect = "小美";
// 中间可能发生很多事情...
boolean success = gf.compareAndSet(expect, "新女友");
System.out.println("线程1操作结果:" + success); // 可能成功,但...
}).start();
// 线程2:搞事情的第三者
new Thread(() -> {
gf.set("小丽"); // 暂时换人
gf.set("小美"); // 又换回来了
}).start();
}
}
2.2 如何解决ABA问题?
Java提供了AtomicStampedReference,给值加上"版本号":
java
// 使用版本号解决ABA问题
AtomicStampedReference<String> stampedGf =
new AtomicStampedReference<>("小美", 0);
// 要修改时,必须值和版本号都匹配
boolean success = stampedGf.compareAndSet(
"小美", "新女友",
0, // 期望的版本号
1 // 新版本号
);
这样,即使值相同,版本号不同也会操作失败!
三、ConcurrentHashMap的进化史
3.1 JDK 1.7时代的"分段锁"策略
JDK 1.7的ConcurrentHashMap像个大型超市,把商品分区管理:
java
// JDK 1.7的结构示意图
ConcurrentHashMap {
Segment[] segments; // 16个区域(默认)
static class Segment {
HashEntry[] table; // 每个区域有自己的哈希表
ReentrantLock lock; // 每个区域有独立的锁
}
}
工作原理:
- 把数据分成16个Segment(可以理解为16个货架区)
- 每个Segment有自己的锁
- 操作不同Segment时不会互相阻塞
优点: 写操作只需要锁住对应的Segment,提高了并发度
缺点: 查询时需要遍历整个Segment,效率有待提升
3.2 JDK 8的华丽转身
JDK 8的ConcurrentHashMap进行了彻底的重构,变得更加优雅:
3.2.1 数据结构变革
java
// JDK 8的ConcurrentHashMap
ConcurrentHashMap {
Node[] table; // 类似HashMap的数组+链表/红黑树
volatile int sizeCtl; // 控制标识符
}
3.2.2 核心改进亮点
1. 抛弃分段锁,使用CAS+synchronized
java
// JDK 8的putVal方法核心逻辑
final V putVal(K key, V value, boolean onlyIfAbsent) {
// ...
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 初始化表,使用CAS
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 如果位置为空,使用CAS添加新节点
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 协助扩容
else {
synchronized (f) { // 对头节点加锁
// 链表或树的操作
}
}
}
// ...
}
2. 引入红黑树解决哈希冲突
当链表长度超过8时,转换为红黑树:
less
链表: A → B → C → D → E → F → G → H
超过8个节点时:
红黑树:
D
/ \
B F
/ \ / \
A C E G H
3. 扩容机制的优化
- 多线程协同扩容:线程在put时如果发现正在扩容,会协助转移数据
- 更精细的锁粒度:只锁住当前操作的桶(链表头节点)
3.2.3 JDK 8 ConcurrentHashMap的亮点总结
特性 | 说明 | 优势 |
---|---|---|
CAS操作 | 初始化、空桶插入使用CAS | 无锁化,提高性能 |
synchronized | 对头节点加锁 | 锁粒度更细 |
红黑树 | 链表过长时转换 | 查询效率从O(n)提升到O(log n) |
协助扩容 | 多线程共同完成数据迁移 | 避免单线程扩容的瓶颈 |
四、实战技巧与面试宝典
4.1 使用示例
java
// ConcurrentHashMap的基本使用
ConcurrentHashMap<String, Integer> userScores = new ConcurrentHashMap<>();
// 线程安全的put
userScores.put("张三", 100);
// 使用computeIfAbsent原子操作
userScores.computeIfAbsent("李四", k -> {
// 复杂的初始化逻辑
return calculateInitialScore(k);
});
// 统计所有分数
int total = userScores.reduceValues(2, Integer::sum);
4.2 面试常见问题
-
ConcurrentHashMap在JDK7和JDK8的区别?
- 数据结构:分段锁 vs 数组+链表/红黑树
- 锁机制:ReentrantLock vs synchronized+CAS
- 并发度:16个Segment vs 桶级别的锁
-
ConcurrentHashMap的size方法如何实现?
- JDK7:尝试2次不锁统计,如果变化就加锁统计
- JDK8:使用baseCount和CounterCell[]来分片计数
-
为什么JDK8用synchronized代替ReentrantLock?
- synchronized在JDK6后做了大量优化,性能接近ReentrantLock
- 减少内存开销,简化实现
总结
从JDK7到JDK8,ConcurrentHashMap完成了一次华丽的转身:
- JDK7像是分工明确的工厂,每个车间有自己的门锁
- JDK8则升级成了现代化智能仓库,用更精细的管控和智能算法提升效率
CAS作为无锁编程的基石,虽然要小心ABA这样的"陷阱",但确实是构建高性能并发容器的利器。
希望这篇博客能让你对CAS和ConcurrentHashMap有更深入的理解。下次面试再被问到这些问题,你就可以自信地说:"这得从CAS的原理说起..."
记住:好的并发设计,就像好的团队协作,既要各司其职,也要默契配合!
PS: 如果在生产环境中使用ConcurrentHashMap,记得结合具体业务场景选择合适的并发级别和初始容量哦!