Java 如何";“安全发布”;延迟生成的有效不可变数组
Java当前的内存模型保证,如果对对象“George”的唯一引用存储在另一个对象“Joe”的Java 如何";“安全发布”;延迟生成的有效不可变数组,java,multithreading,final,Java,Multithreading,Final,Java当前的内存模型保证,如果对对象“George”的唯一引用存储在另一个对象“Joe”的final字段中,并且George和Joe都从未被任何其他线程看到,在存储之前对George执行的所有操作都将被所有线程视为在存储之前执行。如果在final字段中存储对对象的引用是有意义的,那么在这种情况下,这种方法非常有效,此后对象将永远不会发生变化 在假定延迟创建可变类型的对象的情况下(在拥有对象的构造函数完成执行后的某个时间),是否有任何有效的方法来实现这种语义?考虑一个相当简单的类 AlrayOu
final
字段中,并且George和Joe都从未被任何其他线程看到,在存储之前对George执行的所有操作都将被所有线程视为在存储之前执行。如果在final
字段中存储对对象的引用是有意义的,那么在这种情况下,这种方法非常有效,此后对象将永远不会发生变化
在假定延迟创建可变类型的对象的情况下(在拥有对象的构造函数完成执行后的某个时间),是否有任何有效的方法来实现这种语义?考虑一个相当简单的类<代码> AlrayOuts<代码>,它封装了一个不可变的数组,但是它提供了一个方法(三个版本具有相同的标称目的)来返回指定元素之前的所有元素的和。在本例中,假设许多实例将在不使用该方法的情况下构造,但在使用该方法的实例上,将大量使用该方法;因此,在构建数组的每个实例时,不值得预先计算总和,但值得缓存它们
class ArrayThing {
final int[] mainArray;
ArrayThing(int[] initialContents) {
mainArray = (int[])initialContents.clone();
}
public int getElementAt(int index) {
return mainArray[index];
}
int[] makeNewSumsArray() {
int[] temp = new int[mainArray.length+1];
int sum=0;
for (int i=0; i<mainArray.length; i++) {
temp[i] = sum;
sum += mainArray[i];
}
temp[i] = sum;
return temp;
}
// Unsafe version (a thread could be seen as setting sumOfPrevElements1
// before it's seen as populating array).
int[] sumOfPrevElements1;
public int getSumOfElementsBefore_v1(int index) {
int[] localElements = sumOfPrevElements1;
if (localElements == null) {
localElements = makeNewSumsArray();
sumOfPrevElements1 = localElements;
}
return localElements[index];
}
static class Holder {
public final int[] it;
public Holder(int[] dat) { it = dat; }
}
// Safe version, but slower to read (adds another level of indirection
// but no thread can possibly see a write to sumOfPreviousElements2
// before the final field and the underlying array have been written.
Holder sumOfPrevElements2;
public int getSumOfElementsBefore_v2(int index) {
Holder localElements = sumOfPrevElements2;
if (localElements == null) {
localElements = new Holder(makeNewSumsArray());
sumOfPrevElements2 = localElements;
}
return localElements.it[index];
}
// Safe version, I think; but no penalty on reading speed.
// Before storing the reference to the new array, however, it
// creates a temporary object which is almost immediately
// discarded; that seems rather hokey.
int[] sumOfPrevElements3;
public int getSumOfElementsBefore_v3(int index) {
int[] localElements = sumOfPrevElements3;
if (localElements == null) {
localElements = (new Holder(makeNewSumsArray())).it;
sumOfPrevElements3 = localElements;
}
return localElements[index];
}
}
类数组{
最终int[]主数组;
数组内容(int[]initialContents){
mainArray=(int[])initialContents.clone();
}
公共整数getElementAt(整数索引){
返回主数组[索引];
}
int[]makeNewSumsArray(){
int[]temp=newint[mainArray.length+1];
整数和=0;
对于(int i=0;i您的第三个版本不起作用。为存储在final
实例字段中的正确构造的对象所做的保证仅适用于该final
字段的读取。由于其他线程不读取该final
变量,因此没有做出任何保证
最值得注意的是,数组的初始化必须在数组引用存储到最终持有者之前完成。它变量没有说明sumofevelopments3
变量的写入时间(如其他线程所示)。在实践中,JVM可能会优化整个Holder
实例创建,因为它没有副作用,因此生成的代码的行为类似于int[]
数组的普通不安全发布
要使用final
字段发布保证,您必须发布包含final
字段的Holder
实例,无法绕过它
但是如果这个额外的实例让你恼火,你应该考虑使用一个简单的<代码> Value变量。毕竟,你只假设了那个<代码> Value变量的代价,换言之,考虑过早优化。
毕竟,检测另一个线程所做的更改并不需要花费太多,例如,在x86上,它甚至不需要访问主内存,因为它具有缓存一致性。也可能是优化器检测到,一旦变量变为非空,您就再也不会写入该变量,然后启用几乎所有的优化一旦读取了非
null
引用,普通字段的sible就可以了
所以结论总是:测量,不要猜测。只有当你发现真正的瓶颈时才开始优化。我认为你的第二个和第三个例子确实有效(就像你说的那样,引用本身可能不会被另一个线程注意到,这可能会重新分配数组。这是大量额外的工作!)
但这些例子基于一个错误的前提:一个volatile
字段要求读者“注意”这一点是不正确的变化。事实上,volatile
和final
字段执行完全相同的操作。volatile
或final
的读取操作在大多数CPU架构上没有开销。我相信写volatile
时有少量额外开销
所以我只想在这里使用volatile,而不用担心你所谓的“优化”。速度上的差异,如果有的话,将是非常微小的,我说的就像是用总线锁写的额外4个字节。你的“优化”代码读起来非常糟糕
次要的一点是,final字段并不要求您拥有对对象的唯一引用以使其不可变和线程安全。规范只要求您防止对对象进行更改。当然,拥有对对象的唯一引用是防止更改的一种方法。但是,已经不可变的对象(例如java.lang.String
)可以毫无问题地共享
总之:。不要再胡说八道了,只需编写一个简单的数组更新,并将其赋值给volatile
volatile int[] sumOfPrevElements;
public int getSumOfElementsBefore(int index) {
if( sumOfPrevElements != null ) return sumOfPrevElements[index];
sumOfPrevElements = makeNewSumsArray();
return sumOfPrevElements[index];
}
如果线程#1存储一个对易失性字段的引用,然后在某个任意线程X上运行的一段代码将该引用复制到一个“普通”字段字段,然后由线程#2读取,发生之前关系是否仍然适用?如果X为2,它显然适用;如果X为3,我看不出线程3如何复制线程1尚未编写的引用。如果X恰好为1,则保证可能较弱,但当引用是由外部线程复制的,如果它是由创建它的线程复制的,则它将不起作用。“度量值不猜测”的问题它假定程序员的机器与运行程序的所有机器一样。x86内存模型在硬件中实现起来相当昂贵,Java的设计允许程序在硬件更简单的机器上运行。在今天的PC上,易失性读取是便宜的