我们可以通过分析String类的实现具体细节来展示一个final变量是如何可以改变的。
String 对象包含了三个字段: 一个character数组,一个数组的offset和一个length。实现String类的基本原理为:它不仅仅拥有character数组,而且为了避免多余的对象分配和拷贝,多个String和StringBuffer对象都会共享相同的character数组。因此,String.substring()方法能够通过改变length和offset,而共享原始的character数组来创建一个新的String。对一个String来说,这些字段都是final型的字段。
String s1 = "/usr/tmp";
String s2 = s1.substring(4);
字符串s2的offset的值为4,length的值为4。但是,在旧的内存模型下,对其他线程来说,看到offset拥有默认的值0是不可能的,而且,稍后一点时间会看到正确的值4,好像字符串的值"/usr"变成了"/tmp"一样。
旧的Java内存模型允许这些行为,部分JVM已经展现出这样的行为了。在新的Java内存模型里面,这些是非法的。
在新的Java内存模型中,final字段是如何工作的
一个对象的final字段值是它的构造方法里面设置的。假设对象被正确的构造了,一旦对象被构造,在构造方法里面设置给final字段的值在没有同步的情况下对所有其他的线程都会可见。另外,引用这些final字段的对象或数组都将会看到final字段的最新值。
对一个对象来说,被正确的构造是什么意思呢?简单来说:
它意味着这个正在构造的对象的引用在构造期间没有被允许逸出。(参见安全构造技术)。
换句话说,不要让其他线程在其他地方能够看见一个构造期间的对象引用。不要指派给一个静态字段,不要作为一个listener注册给其他对象等等。这些操作应该在构造方法之后完成,而不是构造方法中来完成。
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 j = f.x;
int j = f.y;
}
}
}
上面的类展示了final字段应该如何使用。一个正在执行reader方法的线程保证看到f.x的值为3,因为是final字段。它不保证看到f.y的值为4,因为f.y不是final字段。如果FinalFieldExample的构造方法像这样
public FinalFieldExample() { // bad!
x = 3;
y = 4;
// bad construction - allowing this to escape
global.obj = this;
}
那么,从global.obj
中读取this
的引用,线程不能保证读取到的x的值为3.
能够看到字段的正确的构造值很不错,但是,如果字段本身就是一个引用,那么,你还是希望你的代码能够看到引用所指向的这个对象的最新值。如果字段是final字段,那么这是能够保证的。所以当一个final指针指向一个数组,你不需要担心线程能够看到引用的最新值却看不到引用所指向的数组的最新值。强调一下,这的“正确的”意思是“对象构造方法结尾的最新的值”而不是“最新可用的值”。
现在,在讲了如上的这段之后,如果在一个线程构造了一个不可变对象之后(对象仅包含final字段),你希望保证这个对象被其他线程正确的查看,你仍然需要使用同步才行。例如,没有其他的方式可以保证不可变对象的引用将被第二个线程看到。使用final字段的程序应该仔细的调试,这需要深入而且仔细的理解并发在你的代码中是如何被管理的。
如果你使用JNI来改变你的final字段,这方面的行为是没有定义的。
网友评论