线程安全集合 vs 非线程安全集合:本质区别在哪
核心区别不是“能不能用”,而是“多线程同时读写时会不会崩”。
ConcurrentDictionary<string int></string>允许多个线程同时
Add、
TryGetValue、
Remove,不加锁也不抛
InvalidOperationException;而
Dictionary<string int></string>在同样场景下大概率崩溃——不是每次必现,但只要发生,就是数据错乱或
Collection was modified异常。
System.Collections.Concurrent 里的集合怎么做到“不加锁也安全”
它们不用粗粒度的全局锁(比如整个
lock(_dict)),而是靠细粒度控制 + 无锁原子操作:
ConcurrentQueue<t></t>和
ConcurrentStack<t></t>完全不用锁,靠
Interlocked.CompareExchange等 CPU 原子指令完成入队/出栈
ConcurrentDictionary<tkey tvalue></tkey>把内部哈希桶分段(默认 31 段),写不同段互不影响;同一段内才用轻量级
SpinLock,避免线程挂起开销
ConcurrentBag<t></t>为每个线程维护本地队列(
ThreadLocalList),添加/取自己线程的数据几乎零竞争;跨线程“偷取”时才做同步
所以它不是“慢但稳”,而是“快且稳”——尤其在高并发、读写混合场景下,比手动用
lock包裹
List<t></t>性能高出数倍。
别误用 Synchronized 包装器:它不是线程安全的“快捷方式”
ArrayList.Synchronized或
Hashtable.Synchronized这类老式包装器,只是给每个方法加了同一个对象锁。问题很实在: 所有操作(
Add、
Remove、
Count、遍历)都抢同一把锁 → 严重串行化,吞吐量暴跌 看似“线程安全”,但像
if (list.Count > 0) item = list[0];这种两步操作,中间可能被其他线程修改 → 仍是竞态条件(race condition) 它属于
System.Collections非泛型体系,还有装箱/拆箱开销和类型不安全问题
结论:新项目里完全不要用
Synchronized包装器,直接上
System.Collections.Concurrent的泛型并发集合。
什么时候该用非线程安全集合
不是“不能用”,而是“不该在共享上下文中用”。适用场景非常明确:
单线程逻辑:比如 MVC Controller 里构造一个List<t></t>做临时计算,返回前就丢弃 只读共享:多个线程只调用
ReadOnlyCollection<t>.AsReadOnly(list)</t>或
IEnumerable<t></t>遍历,且确保源集合创建后不再修改 局部变量 or 方法内集合:生命周期严格绑定当前线程栈,无跨线程传递 性能敏感且已用
lock/
ReaderWriterLockSlim手动保护的临界区(此时用
Dictionary可能比
ConcurrentDictionary更省内存)
最容易踩的坑是:以为“我只读不写就安全”,结果忘了某个地方悄悄调了
ToList()或
ToArray(),返回的是新集合——但原始集合若还在被其他线程写,那这个“只读快照”本身就不一致。
真正要记住的不是“哪个类安全”,而是“谁在访问、怎么访问、生命周期归谁管”。
ConcurrentBag不是万能解药,
List<t></t>也不是洪水猛兽——关键在上下文。
