编辑:任何遇到此问题的人,我鼓励您查看讨论。关于读者线程修改数组列表中的对象的观点对于这一类关于共享集合的问题非常重要(以及为什么在这种情况下我可能会选择“只是在整个事情上放一个大锁”而不是我在这里给出的没有对这些修改进行详细分析的方法。不过,以下方法仍然适用于某些情况:
对于您描述的读写模式,我将执行以下操作。
假设 ArrayList 被称为al。对于所有阅读,我会(主要是,见下文)只是从忽略所有线程安全的情况下阅读(别担心,我的疯狂中有方法)。要写我会这样做:
ArrayList temp = new ArrayList(al);//create copy of al
/* code to update temp goes here */
al = temp;//atomic overwrite of al.
Thread.MemoryBarrier();//make sure next read of al isn't moved by compiler or from stale cache value.
al 的复制是安全的,因为 ArrayList 对多个阅读器是安全的。赋值是安全的,因为我们覆盖了引用,它是原子的。我们只需要确保存在内存屏障,尽管在大多数当前系统的实践中我们可能会跳过它(但不要,理论上至少它是必需的,我们不要猜测它)。
这对于大多数共享使用 arraylist 来说是非常可怕的,因为它使写入的成本比它们必须的要高得多。但是,正如您所说,这些频率比读取频率低 60,000,000 倍,因此在这种情况下,这种平衡是有意义的。
使用这种方法进行安全阅读的注意事项:
由于考虑了一个类似的案例,我做出了错误的假设。
以下是安全的:
for(object obj in al)
DoSomething(obj);
即使DoSomething 可能需要很长时间。原因是,这会调用 GetEnumerator() 一次,然后在一个枚举器上工作,即使 al 在此期间发生变化(它会过时,但安全)。
以下内容不安全:
for(int i = 0; i < al.Count; ++i)
DoSomething(al[i]);//
Count 是安全的,但在您到达 al[i] 时它可能已经过时,这可能意味着您在 al 只有 20 个项目时调用 al[43]。
调用BinarySearch、Clone、Contains、IndexOf和GetRange也是安全的,但使用BinarySearch、Contains或IndexOf的结果来决定并不安全你对al 做的第二件事,因为到那时它可能已经改变了。
以下也是安全的:
ArrayList list = al;
for(int i = 0; i != list.Count; ++i)
DoSomething(list[i]);
DoSomethingElse(list);
DoSomethingReallyComplicated(list);
for(int i = 0; i != list.Count; ++i)
SureWhyNotLetsLoopAgain(list[i]);
请注意,我们不是复制al,只是复制引用,所以这真的很便宜。我们对list 所做的一切都是安全的,因为如果它变得陈旧,它仍然是旧列表的一个线程自己的版本,不会出错。
所以。要么在foreach 中执行所有操作(我错误地假设了这种情况),然后将单个调用的结果返回到上面提到的安全方法(但不要使用它来决定是否对al 执行另一个操作) ,或将al 分配给局部变量并对其进行操作。
另一个真正重要的警告是,编写器线程是否会实际设置任何属性或调用任何非线程安全的方法在数组列表中包含的任何对象上。如果是这样,那么您已经从一个需要考虑的线程同步问题变成了几十个!
如果是这种情况,那么您不仅要担心数组列表,还要担心每个可以更改的对象(通过更改我的意思是在 StringBuilder 上调用 .Append("abc") 的方式会更改该对象,而不是替换为一个全新的对象 str = "abc" 更改 str)。
有四种安全的可能性:
- 您的数组列表中有不可变对象 - 快乐的日子。
- 您有可变对象,但不要改变它们 - 也很好,但您必须确定。
- 您有线程安全的对象 - 好的,但这确实意味着您至少有一组线程问题比这个问题处理的问题更复杂。
- 您锁定每个对象以进行写入和读取 - 虽然使用如此精细的锁不太可能出现争用锁,但这有其自身的重量,实际上您并不比默认(并且应该是默认)答案更好的“只是锁,无论如何,锁在没有争议的情况下很便宜)。