假设我正在设计一个包装内部集合的线程安全类:
public class ThreadSafeQueue<T>
{
private readonly Queue<T> _queue = new Queue<T>();
public void Enqueue(T item)
{
lock (_queue)
{
_queue.Enqueue(item);
}
}
// ...
}
Run Code Online (Sandbox Code Playgroud)
基于我的另一个问题,上面的实现是错误的,因为当它的初始化与其使用同时执行时可能会出现种族危险:
ThreadSafeQueue<int> tsqueue = null;
Parallel.Invoke(
() => tsqueue = new ThreadSafeQueue<int>(),
() => tsqueue?.Enqueue(5));
Run Code Online (Sandbox Code Playgroud)
上面的代码是可接受的非确定性的:该项目可能会或可能不会入队.但是,在当前的实现中,它也被破坏了,并且可能引起不可预测的行为,例如抛出IndexOutOfRangeException
,NullReferenceException
多次将相同的项目入队,或者陷入无限循环.这是因为Enqueue
调用可能在将新实例分配给局部变量tsqueue
之后但在内部_queue
字段的初始化完成(或似乎完成)之前运行.
Per Jon Skeet:
在将对新对象的引用分配给实例之前,Java内存模型不能确保构造函数完成.Java内存模型经历了1.5版的重新加工,但是在没有volatile变量的情况下,双重检查锁定仍然被破坏(如在C#中).
可以通过向构造函数添加内存屏障来解决此种族危险:
public ThreadSafeQueue()
{
Thread.MemoryBarrier();
}
Run Code Online (Sandbox Code Playgroud)
同样,通过使字段变化可以更简洁地解决:
private volatile readonly Queue<T> _queue = new …
Run Code Online (Sandbox Code Playgroud)