【并发编程核心】揭秘 ConcurrentHashMap 并发安全的底层黑科技:不只是加锁那么简单!
导语在看过上篇《 ConcurrentHashMap 底层实现演进》后我们知道了 JDK 1.8 为了极限榨取性能大胆地丢弃了原本用来保平安的Segment分段锁结构把地图重新拍扁成了毫无遮拦的Node数组。那么问题来了面对极其凶险的多线程高并发场景没有了 Segment 这个保镖JDK 1.8 的 ConcurrentHashMap 究竟是如何做到金身不坏的今天我们就来扒一扒它背后的“并发安全核心机制”这也正是大厂面试官最爱深挖的硬核区。️ 一、 回顾JDK 1.7 是如何保证安全的对于 JDK 1.7 的ConcurrentHashMap它的安全机制非常好理解依赖于 ReentrantLock。实现原理在这个版本中它将容器切分成了多个Segment。最精妙的是这个Segment类直接继承了ReentrantLock可重入锁。如何生效当一个线程想要往某个 Hash 对应的HashEntry链表里添加数据时必须先调用lock()方法把这个Segment锁住。锁住之后其他试图进入这段区域的线程全被挡在外头直到操作完成你释放了锁才行。局限性虽然比全局锁HashTable快多了但它归根结底还是用了重量级的悲观锁机制。锁的竞争开销依然存在。小白通俗易懂解释1.7 的机制就像是大老板划了 16 个片区每个片区的经理手里都有一把大铁锁。你要向他的地盘扔货经理先当着别人的面把区域门锁上你慢慢扔别人只能在外面排队瞪眼干等。⚔️ 二、 JDK 1.8 的核武器CAS 乐观锁战术既然 1.8 拆除了 16 个片区的墙那怎么在露天广场上维持秩序呢JDK 1.8 亮出了底牌CAS (Compare-And-Swap) 机制。当你想把一对键值对K-V放进数组时系统绝不会上来就用铁锁把你拦住而是采取极其精细的微操策略。场景一所算的数组格子恰好是“空的”无哈希冲突假设你要放入的数据经过 Hash 计算刚好落在数组索引为 5 的这把椅子上而且这把椅子是空的值为 null。这个时候根本不需要锁ConcurrentHashMap会直接调用底层的Unsafe机制发起一次CAS操作相比Compare)系统在一瞬间飞速检查索引 5 的位置“现在”是不是还是空的替换Swap/Set如果确实没人占座就直接以原子级别的速度把当前新来的节点Node一屁股坐进去。失败重试要是在“比较”的那万分之一秒内有个手快的别的线程抢先放了数据进来CAS就会发现座位由于被人占了而失败。这时系统也不会崩溃而是进入自旋循环重试接下来的其他策略比如下面的场景二。小白通俗易懂解释CAS 就像你去电影院找座位。你看到 5 号座是空的你不会傻乎乎地去大厅申请“找座排队许可证重量锁”只管直接冲过去坐下但由于有多个人可能同时冲向这把椅子坐下的那一瞬间你的屁股如果探到椅子还是空的你就赢了写入成功。如果发现别人已经抢先落座了你也只是笑笑放弃这次尝试重新排队。全程无痛极限丝滑 三、 遇到冲突如何保底 Synchronized 登场乐观锁虽然轻快但如果是坏情况呢场景二数组格子里已经有人了发生哈希冲突如果多个线程的 Hash 算出来的索引一模一样或者刚才那个座位已经被填上了数据形成了一条细长的链表怎么办此时必须回归悲观防御。但 1.8 将锁的粒度压缩到了极致它只锁住这条链表的“头节点”或者树的根节点使用的是大家最熟悉的底层原生关键字synchronized。操作流程当发现该格子不为空时。线程直接提取这个格子里的第一个元素即该单向链表的 Head 节点。使用synchronized (head)把这个头节点紧紧锁住。上锁后安心顺着链表往下找位置插入或更新数据。小白通俗易懂解释如果 5 号摊位已经有人排起了一条长队哈希冲突产生链表为了不插队打架系统不再允许你“偷偷摸摸直接坐下去”了而是揪住队伍最前面的那个人头节点当纠察员。给这只队伍最前面加上一把小锁。只有拿到锁的人才能去这条队伍里塞东西。而别忘了隔壁 6 号摊位的队伍是完全另一把独立的锁。如此一来并发粒度被极限拆分到了数组的每一个格子上这就意味着只要数组长度有 1000就能支持 1000 个线程真正地在同时安全写入⚡ 四、 其他隐藏的高阶并发机制1. volatile 关键字的广泛应用可见性护航在 JDK 1.8 的Node节点定义中你可以看到这样的代码staticclassNodeK,VimplementsMap.EntryK,V{finalinthash;finalKkey;volatileVval;// 注意它volatileNodeK,Vnext;// 注意它}无论是节点的值val还是指向下一个节点的指针next统统被volatile修饰。这意味着只要有任何一个线程修改了值或者拉长了链表这个变动会瞬间强制刷入主存其他任何正在读或者准备改的线程能立刻看见最新鲜的状态防止了脏读。也是基于此get读操作是根本不需要加任何锁的2. 协助扩容黑科技ForwardingNode以前的HashMap扩容是个大杀器如果这时候别人来写数据会直接爆炸。而ConcurrentHashMap 1.8发明了一种特殊状态的节点ForwardingNode。如果在你尝试写入发现当前 Map 正在翻新扩容该线程非但不会干等还会“路见不平拔刀相助”一起撸起袖子帮着原本干苦力的线程进行数据迁移工作多线程并行扩容把整体扩容速度拉满。 五、 全局总结速记面试高分必备当面试官问你以 JDK 1.8 为例ConcurrentHashMap 到底是怎么保证并发安全的不要慌抛出这个万能三板斧公式极小粒度架构机制抛弃了 Segment锁的粒度细化到了基于数组中的每一个 Node桶。CAS 乐观首发机制首节点新增没冲突时依赖 JVM 级别的CAS自旋原子操作实现无阻塞极限插入。Synchronized 悲观保底当发生 Hash 冲突需遍历链表/红黑树时利用synchronized只锁定该坑位的 Head 节点隔离影响范围完美平衡了安全与极速。