原文来自于:zha-ge.cn/java/78
ConcurrentHashMap 1.7 vs 1.8:分段锁到 CAS+红黑树的演进与性能差异
讲真,老程序员都得靠点岁月滤镜。我第一次深扒 ConcurrentHashMap 的源码,还是在 JDK 1.7 时代,那时候的哈希并发魂就是"分段锁",每次和同事聊起,都有点"这玩意多神"的尬自豪。结果等到 1.8 出来,我突然尴尬地发现------这货都变天了!今天,就和大家唠唠我踩过的那些并发哈希坑。
老 ConcurrentHashMap 的分段锁江湖
Java 1.7 的 ConcurrentHashMap 纯粹是"分治思想"的现实写照。它把整个 Map 切成多个 Segment,每个 Segment 都是个小 HashTable,管自己一亩三分地。你 put、get、remove,基本不会打架:
- 分段锁(Segment):每段一把锁,减少竞争
- 不支持 null key/null value(冷知识)
比如放数据,大致长这样:
java
int hash = hash(key);
int segmentIndex = (hash >>> segmentShift) & segmentMask;
Segment<K,V> s = segments[segmentIndex];
s.lock();
try {
s.put(key, value);
} finally {
s.unlock();
}
简化后的意思就是:我锁的只是 Segment,不像 HashTable 那样整个表锁死(所以 HashTable 几乎没人用了...)。
JDK 1.8:演变新世界,CAS+链表+红黑树
到了 1.8,这玩意彻底变了个腔调,"分段锁"拜拜,取而代之的是"桶级别"的操作+一身并发黑科技:
- 不再有 Segment 数组,只有 Node[] table
- put 的时候,先 CAS 尝试抢占 Node(用 synchronized 兜底)
- 链表太长自动转红黑树,查找 O(logn) 不是梦
来,来,看核心放数据的套路:
java
Node<K,V>[] tab = table;
int i = (n - 1) & hash;
Node<K,V> f = tabAt(tab, i);
if (f == null) {
if (casTabAt(tab, i, null, newNode)) {
// CAS抢坑成功,无竞争~
}
} else {
synchronized (f) {
// 操作链表或红黑树,争抢得激烈则自动变树!
}
}
注:这 tabAt
跟 casTabAt
都是 Unsafe 的骚操作~
踩坑瞬间
我自己曾经经历过一次性能"大地震"。那会儿线上压测,两个不同 JDK 下 ConcurrentHashMap,居然结果天差地别。
痛点回忆:
- 1.7 多线程 put,性能稳定,但线程数过高还是得拼命锁各个段
- 1.8 急剧提升高并发下的写吞吐,尤其线程超多时,老版本直接锁段,等得人抓耳挠腮,新版 CAS 猛冲
- 红黑树救过命:有一回 keys 奇葩碰撞扎堆,1.7 直接挂了链表超长数秒。1.8 自动转树,时间稳定 O(logn),查找都不带卡的
有次还诡异碰到过遍历 ConcurrentHashMap 一边 put 的场景,1.7 下因为 Segment 结构脑壳痛,1.8 基本溜了。
性能小对比
偷偷摸鱼做了下对比:
JDK | 高并发写延迟 | 键大量冲突 | 遍历一致性 |
---|---|---|---|
1.7 分段锁 | 高 | 查找慢 | 易被锁阻塞 |
1.8 CAS+树 | 低 | 稳定O(logn) | 比较优秀 |
简单一句话:1.7 读写分段锁,跑满高速路还堵车;1.8 脱胎换骨,红绿灯智能变道,加点"算法调度",体验大变脸!
经验启示
- 多线程首选 1.8 及以上,不用 segment 锁省心
- Async 大量写入/高冲突 key,红黑树才是真香代码
- 不想卡链表查找?JDK 1.8 后不怕碰撞怪兽
- 想偷懒?1.8 遍历、并发用法更随心,不用操心锁分段
唉,老了,偶尔还是怀念 Segment 那点"老锁情怀",但终究得向先进技术低头。也许代码本来也是进化史------每一版改动后,费神琢磨"为啥这样",也就有了新技能傍身。大家有啥并发"笑话",咱评论区扯一扯呗?
写到这儿,键盘都热了,今天就聊到这儿吧。忙到凌晨的程序员,才能继续踩下一个坑不是?