concurrentdictionary的“分段锁”实为无锁+细粒度同步混合策略,依赖volatile、interlocked和少量spinwait,仅扩容或cas失败超10次时才使用_locks数组中的monitor锁;segment数自.net 6+起设为4×cpu核心数(≥31),以平衡冲突率与内存开销。

ConcurrentDictionary 的分段锁不是传统意义上的“分段锁”
它压根没用 lock 语句或 Monitor 对每个段加独占锁,而是基于无锁(lock-free)+ 细粒度同步的混合策略。真正起作用的是数组 + volatile 字段 + Interlocked 操作 + 少量 SpinWait,只有在极少数竞争激烈路径(如扩容、首次插入空桶)才会退化为轻量级锁(Lock 内部用的 Monitor)。所以别被“分段锁”字面误导——它不靠多个 lock 对象实现并发控制。
segments 数组和 bucket 分布怎么决定线程是否冲突
初始化时,ConcurrentDictionary 会按构造参数或默认值(如 .NET 6+ 默认 4 * CPU 核心数)创建一个 Node[] 数组,称为 segments(实际字段名是 _buckets,但逻辑上按 segment 划分)。每个 key 经过两次哈希:GetHash(key) 得到原始 hash,再对 _buckets.Length 取模得到 segment 索引。只要两个 key 落在不同 segment,它们的读写就完全无竞争;落在同一 segment 后,才进入该 segment 内部的链表/数组结构处理。
- segment 数量在构造后固定,不随元素增长而动态增加(扩容是重建整个
_buckets数组) - 同一个 segment 内部不设子锁,所有操作通过 CAS(
Interlocked.CompareExchange)更新头节点或 next 引用 - 写操作(
AddOrUpdate、TryRemove)会先尝试无锁插入,失败则自旋重试,多次失败后才可能触发 segment 级锁(_locks[segmentIndex])
_locks 数组只在扩容和极端竞争时才真正起作用
_locks 是一个 object[],长度等于 _buckets.Length,每个元素对应一个 segment 的“后备锁对象”。但它平时几乎不参与同步:
- 读操作(
TryGetValue、ContainsKey)全程无锁,仅靠volatile读和引用可见性保证 - 写操作中,99% 场景下靠
Interlocked原子操作完成节点插入/删除,_locks[seg]不会被持有 - 只有两种情况会真正
lock (_locks[seg]):该 segment 正在扩容中(此时要阻塞写入),或 CAS 自旋超过阈值(默认 10 次)仍失败
也就是说,_locks 是兜底机制,不是主干同步手段。
为什么 .NET 6+ 把 segment 数量从 31 改成 4 × CPU count
老版本(.NET Core 2.x / .NET Framework)硬编码 segment 数为质数 31,容易导致哈希分布不均、热点 segment 集中。新版本改为 Math.Max(4 * Environment.ProcessorCount, 31),核心目标是让 segment 数与真实并发线程数匹配:
- segment 太少 → 多个线程挤进同一 segment,CAS 冲突率上升,自旋开销变大
- segment 太多 → 内存占用增加(每个 segment 至少一个空链表头),且 cache line false sharing 风险上升
- 4×CPU count 是经验值:既覆盖常见线程池线程数,又留出余量应对突发并发
internal static int GetDefaultConcurrencyLevel() =>
Math.Max(4 * Environment.ProcessorCount, 31);分段设计的关键不在“锁”,而在“隔离哈希空间”。真正难懂的部分是它如何用纯原子操作模拟链表修改——比如插入时怎么保证 head 更新和 next 指针设置的原子性,这依赖于 Node 结构体的不可变性和 Interlocked.CompareExchange 的循环重试逻辑。这些细节一旦写错,就会出现 ABA 问题或内存泄漏,所以别轻易自己仿写。










