如何实现列表并发线程安全访问及锁外对象引用的线程安全性问题
列表并发安全与锁内存可见性问题解答
咱们先解决第一个问题:如何实现对列表的并发、线程安全访问?
常见的实现方式有这么几种:
- 用现成的线程安全集合:比如.NET框架提供的
ConcurrentBag<T>、ConcurrentQueue<T>、ConcurrentStack<T>这些,它们内部已经封装了线程同步的逻辑,不用咱们自己手动加锁,基本能覆盖大部分并发读写的场景。 - 手动加互斥锁保护:就像你示例里用
lock关键字那样,把所有对列表的读写操作都包裹在同一个锁对象的lock块里。这样能保证同一时刻只有一个线程能操作列表,从根本上避免并发修改导致的异常或者数据错乱。 - 用读写锁优化性能:如果你的场景是读操作远多于写操作,可以用
ReaderWriterLockSlim。它允许多个线程同时读取列表,只有写操作的时候会独占锁,这样能比普通的lock获得更好的并发性能。
接下来咱们针对第二个问题和你的示例代码展开分析:
锁内取引用、锁外使用的内存可见性与线程安全问题
首先要明确:.NET里的lock语句不只是做线程互斥,它还自带内存屏障的效果:
- 当线程进入
lock块时,会强制刷新处理器缓存,确保读取到的是主内存里的最新数据; - 当线程退出
lock块时,会把当前线程缓存里的所有修改写回主内存,同时让其他线程的对应缓存失效。
结合这个特性,咱们逐个解答你的疑问:
会不会出现处理器缓存的过期数据?
不会。在TimerElapsed的lock块里获取l = list的引用时,内存屏障已经保证这个引用以及列表内部的所有状态都是最新的。而且题目里明确说其他线程在锁释放后不会再使用l这个对象,所以锁外操作l的时候,它的状态不会被其他线程修改,处理器缓存里的l数据是完全可靠的,不会过期。能否看到获取锁之前其他线程做出的所有修改?
完全可以。所有Write方法的修改都是在同一个lock块里完成的,每次Write执行完lock块退出时,都会把修改同步到主内存。当TimerElapsed进入lock块时,内存屏障会强制刷新缓存,所以获取到的l必然包含了之前所有Write线程的修改。整体是否线程安全?
在你这个场景下是完全线程安全的:
Write方法每次操作列表都加锁,确保多个线程同时写的时候不会破坏列表的内部结构;TimerElapsed在锁内完成列表的切换逻辑,保证切换过程中不会和Write的操作冲突,不会出现一半切换完一半还在写的中间状态;- 锁外处理
l的时候,l已经被切换成“闲置”的列表了,Write方法只会操作新的list,所以锁外的l.Clear()等操作不会有并发修改的问题。
最后一个具体问题:能否确保l总能看到自上次TimerElapsed以来的所有新增内容?
答案是肯定的,原因如下:
- 两次
TimerElapsed执行之间,所有Write的新增操作都在当前活跃的list上进行,而每个Write都通过lock保证修改被同步到主内存; - 当本次
TimerElapsed进入lock块时,内存屏障确保读取到的l(也就是之前的活跃列表)是包含了所有自上次以来Write修改的最新状态; - 列表切换的操作在锁内完成,切换过程中不会有新的
Write操作到l上,所以拿到的l就是完整的自上次执行以来的所有新增内容。
内容的提问来源于stack exchange,提问作者lilo0
相关产品推荐
相关产品推荐

