我有课
private volatile long[][] data = new long[SIZE][];
最初只包含空值和一个访问它的方法。当它遇到空元素时,它会创建一个 long[]
并存储以备将来使用。此操作是幂等的,多个线程在同一元素上浪费时间不是问题。
任何线程都不能看到未完全填充的元素。我希望以下代码能够正确执行:
long[] getOrMakeNewElement(int index) {
long[] element = data[index]; // volatile read
if (element==null) {
element = makeNewElement(index); // local operation
data = data; // ugliness 1
data[index] = element;
data = data; // ugliness 2
}
return element;
}
第一个丑陋之处是确保其他线程理论上可以看到对元素
所做的更改。他们实际上无法访问它,因为它还没有存储。然而,在下一个类似的 element
被存储并且另一个线程可能会也可能不会看到这个存储,所以 AFAIK 第一个丑陋是必要的。
第二个丑陋之处只是确保其他线程看到包含元素
的新data
。这里奇怪的是两次使用丑陋。
这是安全出版的必要和足够吗?
注意:虽然这个问题类似于 this one ,它没有重复,因为它处理修改现有的一维数组而不是创建一个。这使答案一目了然。
更新
注意:这不是生产代码,我知道但不关心替代方案(AtomicReferenceArray
、synchronized
,等等)。我写这个问题是为了更多地了解 JMM。这是一个真实的代码,但仅用于我在 Euler 项目上的胡闹,在此过程中没有动物受到伤害。
我想,一个安全实用的解决方案是
class Element {
Element(int index) {
value = makeNewElementValue(index);
}
final long[] value;
}
private volatile Element[] data = new Element[SIZE];
其中 Element
通过 Semantics of final Fields 确保可见性.
正如 user2357112 所指出的,当多个线程写入相同的 data[index]
时,也存在(恕我直言,无害)数据竞争,这实际上很容易避免。虽然读取必须很快,但创建一个新元素的速度足够慢以允许任何所需的同步。它还将允许更有效地初始化数据。
最佳答案
Java 语言规范对 volatile 的语义定义如下:
A write to a volatile variable
v
synchronizes-with all subsequent reads ofv
by any thread (where "subsequent" is defined according to the synchronization order).
规范保证无论何时一个 Action 发生在另一个 Action 之前,它对另一个 Action 都是可见的。
为了安全发布对象,对象的初始化必须对任何获得该对象引用的线程可见。由于我们感兴趣的字段不是 final 的,因此只有保证对象的初始化发生在任何其他线程获得对该对象的引用之前,才能完成此操作。
为了验证是否是这种情况,让我们看一下所涉及操作的发生前图:
makeNewElement
|
v
read of data
|
v ?
write to data --------------> read of data
| |
v v
write to array element read of array element
| |
v V
read of data useElement
|
v
write to data
显然,当且仅当“数据写入”与“数据读取”同步时,存在从 makeNewElement 到 useElement 的路径,当且仅当读取是后续 .然而,它不需要是后续的:对于读取后续的每个执行,我们可以创建另一个不在后续的执行,只需按同步顺序向后移动读取:
makeNewElement
|
v
read of data read of data
| |
v |
write to data |
| |
v v
write to array element read of array element
| |
v v
read of data useElement
|
v
write to data
通常,我们不能这样做,因为这会改变正在读取的值,但是由于写入不会改变 data
的值,我们无法从读取的值判断读取是否在写之前或之后。
在这样的执行中,对我们新对象的引用是通过数据竞争发布的,因为数组元素的写入不会发生在读取之前。在这种情况下,规范写道:
More specifically, if two actions share a happens-before relationship, they do not necessarily have to appear to have happened in that order to any code with which they do not share a happens-before relationship. Writes in one thread that are in a data race with reads in another thread may, for example, appear to occur out of order to those reads.
也就是说,读取线程可能会看到对新对象的引用,但看不到其初始化的效果,这样就比较糟糕了。
那么,我们如何确保读取是后续的呢?如果从 volatile 字段读取的值证明必要的写入已经发生。在您的情况下,我们需要区分对不同元素的写入。我们可以为每个数组元素使用一个单独的 volatile 变量来做到这一点:
Element[] data = ...;
class Element {
volatile long[] value;
}
long[] getOrMakeNewElement(int index) {
long[] element = data[index].value; // volatile read
if (element==null) {
element = makeNewElement(index); // local operation
data[index].value = element;
}
return element;
}
或为每次写入更改单个 volatile 字段的值:
volatile long[][] data;
long[] getOrMakeNewElement(int index) {
long[] element = data[index]; // volatile read
if (element==null) {
long[][] newData = Arrays.copyOf(data);
newData[index] = element = makeNewElement(index);
data = newData;
}
return element;
}
当然,后一种解决方案的缺点是并发写入即使对于不同的数组元素也会发生冲突。
关于java - 数组的 volatile 数组,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30420782/