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