一点(简化的)上下文。
假设我有一个 ArrayList<ContentStub>
哪里 ContentStub
是:
public class ContentStub {
ContentType contentType;
Object content;
}
我有多个类的实现,每个类“膨胀”存根 ContentType
,例如。
public class TypeAStubInflater {
public void inflate(List<ContentStub> contentStubs) {
contentStubs.forEach(stub ->
{
if(stub.contentType == ContentType.TYPE_A) {
stub.content = someService.getContent();
}
});
}
}
我的想法是 TypeAStubInflater
它只修改项目 ContentType.TYPE_A
在一个线程中运行,并且 TypeBStubInflater
它只修改项目 ContentType.TYPE_B
等等-但每个示例 inflate()
方法正在修改相同中的项 contentStubs
并列列出。
然而:
没有线程可以改变 ArrayList
没有线程试图修改被另一个线程修改的值
没有线程尝试读取由另一个线程写入的值
考虑到所有这些,似乎不需要额外的措施来确保线程安全。从(非常)快速地看 ArrayList
执行,似乎没有风险 ConcurrentModificationException
-然而,这并不意味着其他事情就不会出错。我是错过了什么,还是这样做比较安全?
2条答案
按热度按时间b4lqfgs41#
假设线程a写
TYPE_A
内容和线程b写入TYPE_B
内容。名单contentStubs
仅用于获取ContentStub
:只读。所以从a,b和contentStubs
,没有问题。但是,线程a和b所做的更新可能永远不会被另一个线程看到,例如,另一个线程c可能会得出这样的结论stub.content == null
对于列表中的所有元素。原因是java内存模型。如果不使用锁、同步、volatile和原子变量等构造,那么内存模型就不能保证一个线程对一个对象的修改是否以及何时对另一个线程可见。为了让这个更实用一点,让我们举个例子。
假设线程a执行以下代码:
列表元素17是对
ContentStub
全局堆上的对象。允许vm创建该对象的私有线程副本。对线程a中引用的所有后续访问都使用该副本。vm可以自由决定何时以及是否更新全局堆上的原始对象。现在想象一个线程c执行以下代码:
vm可能会对线程c中的私有副本执行相同的操作。
如果线程c在线程a更新对象之前已经访问了该对象,那么线程c很可能会使用–not updated–副本并在很长一段时间内忽略全局原始副本。但是,即使线程c在线程a更新对象后第一次访问该对象,也不能保证线程a的私有副本中的更改已经在全局堆中结束。
简而言之:如果没有锁或同步,线程c几乎肯定只能读取
null
在每个stub.content
.这种内存模式的原因是性能。在现代硬件上,所有cpu/核的性能和一致性之间存在一种权衡。如果现代语言的内存模型要求一致性,那么很难在所有硬件上保证一致性,而且可能会对性能造成太大的影响。因此,现代语言采用低一致性,并在需要时为开发人员提供显式的构造来实施它。再加上编译器和处理器对指令的重新排序,这使得关于程序代码的老式线性推理变得有趣。
btxsgosb2#
一般来说,这是可行的,因为您没有修改
List
它本身会抛出一个ConcurrentModificationException
如果任何迭代器在循环时处于活动状态,而只是修改列表中的一个对象,那么从列表的pov来看,这是很好的。我建议你把你的工作分成
Map<ContentType, List<ContentStub>>
然后用这些特定的列表开始线程。您可以使用以下命令将列表转换为Map:
如果您的列表不是那么大(<1000个条目),我甚至建议不要使用任何线程,而只是使用一个普通的for-i循环来迭代,甚至
.foreach
如果这两个额外的整数没有关系。