关于线程安全,最终字段真的有用吗?
几年来,我每天都在使用Java内存模型。我认为我对数据竞赛的概念以及避免它们的不同方法(例如,同步块,易失性变量等)有很好的理解。但是,对于内存模型,我仍然没有完全理解一些东西,这就是类的最终字段应该是线程安全的,而无需任何进一步的同步。
因此,根据规范,如果一个对象被正确初始化(也就是说,对该对象的引用在其构造函数中没有以一种可以被另一个线程看到引用的方式转义),那么,在构造之后,任何看到该对象的线程都将保证看到对对象的所有最终字段的引用(在构造时的状态下), 无需任何进一步的同步。
特别是,标准(http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4)说:
最终字段的使用模型很简单:在对象的构造函数中设置对象的最终字段;并且不要在对象的构造函数完成之前,在另一个线程可以看到它的位置写入对正在构造的对象的引用。如果遵循此命令,则当另一个线程看到该对象时,该线程将始终看到该对象的最终字段的正确构造版本。它还将看到由最终字段引用的任何对象或数组的版本,这些字段至少与最终字段一样最新。
他们甚至给出了以下例子:
class FinalFieldExample {
final int x;
int y;
static FinalFieldExample f;
public FinalFieldExample() {
x = 3;
y = 4;
}
static void writer() {
f = new FinalFieldExample();
}
static void reader() {
if (f != null) {
int i = f.x; // guaranteed to see 3
int j = f.y; // could see 0
}
}
}
其中,线程 A 应该运行“reader()”,线程 B 应该运行“writer()”。
到目前为止,显然一切都很好。
我主要关心的是...这在实践中真的有用吗?据我所知,为了让线程A(运行“reader()”)看到对“f”的引用,我们必须使用一些同步机制,例如使f易失性,或者使用锁来同步对f的访问。如果我们不这样做,我们甚至不能保证“reader()”能够看到初始化的“f”,也就是说,由于我们尚未同步对“f”的访问,因此读者可能会看到“null”而不是由编写器线程构造的对象。这个问题在 http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#finalWrong 中有所说明,这是Java内存模型的主要参考之一[粗体强调我的]:
现在,说了这么多,如果在线程构造了一个不可变对象(即仅包含最终字段的对象)之后,您希望确保所有其他线程都能正确看到它,则通常仍需要使用同步。例如,没有其他方法可以确保第二个线程可以看到对不可变对象的引用。程序从最终字段获得的保证应该仔细地调整,并深入了解如何在代码中管理并发性。
因此,如果我们甚至不能保证看到对“f”的引用,因此我们必须使用典型的同步机制(易失性,锁等),并且这些机制已经导致数据竞争消失,那么对final的需求是我甚至不会考虑的事情。我的意思是,如果为了使“f”对其他线程可见,我们仍然需要使用易失性或同步块,并且它们已经使内部字段对其他线程可见......首先使字段最终确定的意义(在线程安全术语中)是什么?