我們可以通過分析String類的實現(xiàn)具體細(xì)節(jié)來展示一個final變量是如何可以改變的缩功。
String 對象包含了三個字段: 一個character數(shù)組值骇,一個數(shù)組的offset和一個length。實現(xiàn)String類的基本原理為:它不僅僅擁有character數(shù)組吠卷,而且為了避免多余的對象分配和拷貝鳄哭,多個String和StringBuffer對象都會共享相同的character數(shù)組泰佳。因此柒莉,String.substring()方法能夠通過改變length和offset闻坚,而共享原始的character數(shù)組來創(chuàng)建一個新的String。對一個String來說兢孝,這些字段都是final型的字段窿凤。
String s1 = "/usr/tmp";
String s2 = s1.substring(4);
字符串s2的offset的值為4,length的值為4跨蟹。但是雳殊,在舊的內(nèi)存模型下,對其他線程來說窗轩,看到offset擁有默認(rèn)的值0是不可能的夯秃,而且,稍后一點時間會看到正確的值4痢艺,好像字符串的值"/usr"變成了"/tmp"一樣仓洼。
舊的Java內(nèi)存模型允許這些行為,部分JVM已經(jīng)展現(xiàn)出這樣的行為了腹备。在新的Java內(nèi)存模型里面衬潦,這些是非法的。
在新的Java內(nèi)存模型中植酥,final字段是如何工作的
一個對象的final字段值是它的構(gòu)造方法里面設(shè)置的。假設(shè)對象被正確的構(gòu)造了弦牡,一旦對象被構(gòu)造友驮,在構(gòu)造方法里面設(shè)置給final字段的值在沒有同步的情況下對所有其他的線程都會可見。另外驾锰,引用這些final字段的對象或數(shù)組都將會看到final字段的最新值卸留。
對一個對象來說,被正確的構(gòu)造是什么意思呢椭豫?簡單來說:
它意味著這個正在構(gòu)造的對象的引用在構(gòu)造期間沒有被允許逸出耻瑟。(參見安全構(gòu)造技術(shù))旨指。
換句話說,不要讓其他線程在其他地方能夠看見一個構(gòu)造期間的對象引用喳整。不要指派給一個靜態(tài)字段谆构,不要作為一個listener注冊給其他對象等等。這些操作應(yīng)該在構(gòu)造方法之后完成框都,而不是構(gòu)造方法中來完成搬素。
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字段應(yīng)該如何使用。一個正在執(zhí)行reader方法的線程保證看到f.x的值為3魏保,因為是final字段熬尺。它不保證看到f.y的值為4,因為f.y不是final字段谓罗。如果FinalFieldExample的構(gòu)造方法像這樣
public FinalFieldExample() { // bad!
x = 3;
y = 4;
// bad construction - allowing this to escape
global.obj = this;
}
那么粱哼,從global.obj
中讀取this
的引用,線程不能保證讀取到的x的值為3.
能夠看到字段的正確的構(gòu)造值很不錯檩咱,但是揭措,如果字段本身就是一個引用,那么税手,你還是希望你的代碼能夠看到引用所指向的這個對象的最新值蜂筹。如果字段是final字段,那么這是能夠保證的芦倒。所以當(dāng)一個final指針指向一個數(shù)組艺挪,你不需要擔(dān)心線程能夠看到引用的最新值卻看不到引用所指向的數(shù)組的最新值。強(qiáng)調(diào)一下兵扬,這的“正確的”意思是“對象構(gòu)造方法結(jié)尾的最新的值”而不是“最新可用的值”麻裳。
現(xiàn)在,在講了如上的這段之后器钟,如果在一個線程構(gòu)造了一個不可變對象之后(對象僅包含final字段)津坑,你希望保證這個對象被其他線程正確的查看,你仍然需要使用同步才行傲霸。例如疆瑰,沒有其他的方式可以保證不可變對象的引用將被第二個線程看到。使用final字段的程序應(yīng)該仔細(xì)的調(diào)試昙啄,這需要深入而且仔細(xì)的理解并發(fā)在你的代碼中是如何被管理的穆役。
如果你使用JNI來改變你的final字段,這方面的行為是沒有定義的梳凛。