我先前对您的问题有误解,又添加了另一个答案。抱歉。
让我们假设这个:
查看ArrayList源代码,我们可以看到size和elementData字段不是可变的:
transient Object[] elementData; // non-private to simplify nested class access
private int size;
另外,让我们看一下add方法:
/**
* This helper method split out from add(E) to keep method
* bytecode size under 35 (the -XX:MaxInlineSize default value),
* which helps when add(E) is called in a C1-compiled loop.
*/
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
会发生类似的事情吗?
elementData是否会发生类似情况?
TL; DR:通过正确的同步,您描述的问题是不可能的。
Java具有相当复杂的内存模型。 JVM可以自由地在内部对语句进行重新排序,以便更有效地执行它们,但要遵守这样的约束条件:您必须知道该约束可以告诉线程内的操作已重新排序。
[本质上,这就像一个老板说:“我不在乎您如何完成工作,只需要在[一段时间]内完成[工作]。]]
困难之处在于,尽管它说您一定不能看到线程内的重新排序,但它并不意味着不同的线程不能看到彼此以不同的顺序进行工作。
这是令人头晕的东西。简化的概念是happens-before
的思想。您可以在两个线程中做某些事情,以确保一个线程完成的事情在另一线程尝试使用它们的结果时似乎已经发生。从字面上看,一个线程中的事物已“发生在”另一个线程中的事物“之前”。有许多众所周知的事物会在关系发生之前发生:
因此,易失性和同步都是创建事件的两种方法,这是保证一个线程完成的操作(某些事情)被另一线程看到的必要条件。>>
但是两者之间有区别:
在添加到ArrayList
的情况下,需要原子性,因为您要做的不只是一件事情:增加大小和分配新的数组元素。
也使变量易失性在正确性方面毫无用处,但在模态情况下会使代码变慢,在模态情况下,ArrayList
只能从单个线程访问。
因此,只要您的代码正确
同步-也就是说,对列表的所有访问都在同一件事上同步,例如在列表本身上-由于原子性和可见性,您描述的情况不会发生同步属性。我先前对您的问题有误解,又添加了另一个答案。抱歉。
关于ArrayList中size字段的波动性,您的担心是合理的。您描述的错误是可能的。这是an article describing exactly the same issue。
volatile
引用对象的情况下,请确保该引用本身将及时对其他线程可见,但对于其成员变量而言,情况并非如此。如果单独访问,不能保证对象中包含的数据将始终可见。对于
java.util
中的标准ArrayList,您无能为力,但是您可以通过使自己的类与ArrayList紧密镜像,但正确设置实例变量的可变性,来轻松解决此问题。这也可以为您提供一种更简单(或更易于阅读)的方式来同步列表,而不是在外部进行同步。
我先前对您的问题有误解,又添加了另一个答案。抱歉。