Interlocked.CompareExchange内存顺序异常导致无锁对象池故障排查
问题原因拆解
你碰到的核心问题是普通字段的内存可见性缺失,和Interlocked.CompareExchange本身的内存屏障没关系,具体拆解:
1. 为什么Interlocked.CompareExchange和手动加MemoryBarrier没用?
Interlocked.CompareExchange确实是全内存屏障,能挡住它前后的指令重排,也能把自身的读写同步到主存。但问题出在cmd.Data = null这个普通字段的写入上:
- 普通字段的写入默认没有强制同步到主存的要求,CPU可能把这个操作缓存到本地核心的缓存里,没同步到所有线程都能看到的主存
- 同时JIT编译器可能对这个写入做优化,比如偷偷把它重排到CompareExchange之后(哪怕加了MemoryBarrier,普通字段的读写还是没被强制要求可见性)
说白了,你在ReturnCommand里先写了null,但其他线程的RentCommand读Data时,读的是自己CPU缓存里的旧值,根本没看到你写的null。
2. 加[MethodImpl(MethodImplOptions.Synchronized)]为啥就正常?
这个特性本质是给方法套了个独占锁:
- 进方法自动拿锁,退方法自动放锁
- 放锁的时候,会强制把当前线程所有缓存里的写入同步到主存;拿锁的时候,会强制刷新当前线程的缓存,从主存读最新值
- 而且锁的范围会阻止JIT和CPU把锁内的指令重排到锁外面
所以加了这个特性后,ReturnCommand里的cmd.Data = null会在放锁前同步到主存,RentCommand拿锁后自然能读到最新的null值。
3. 正确的无锁解决办法
不用锁也能搞定,只要保证cmd.Data的读写有内存可见性:
- 把
Data字段标记为volatile:public volatile object Data;,这样每次读写都会直接操作主存,不让JIT和CPU做缓存优化 - 或者用
Volatile.Write和Volatile.Read来读写Data:- ReturnCommand里换成
Volatile.Write(ref cmd.Data, null); - RentCommand里换成
var data = Volatile.Read(ref cmd.Data);
- ReturnCommand里换成
这样结合Interlocked.CompareExchange的屏障,就能保证cmd.Data = null的写入对其他线程可见,正确实现你要的happens-before语义。
内容的提问来源于stack exchange,提问作者Roman Sakno
相关产品推荐
相关产品推荐

