摘要:寫域重排序規(guī)則寫域的重排序規(guī)則禁止對域的寫重排序到構(gòu)造函數(shù)之外,這個(gè)規(guī)則的實(shí)現(xiàn)主要包含了兩個(gè)方面禁止編譯器把域的寫重排序到構(gòu)造函數(shù)之外編譯器會(huì)在域?qū)懼螅瑯?gòu)造函數(shù)之前,插入一個(gè)屏障。結(jié)論只有當(dāng)構(gòu)造函數(shù)返回時(shí),引用才應(yīng)該從線程中逸出。
final關(guān)鍵字final的簡介final可以修飾變量,方法和類,用于表示所修飾的內(nèi)容一旦賦值之后就不會(huì)再被改變,比如String類就是一個(gè)final類型的類。
final的具體使用場景final能夠修飾變量,方法和類,也就是final使用范圍基本涵蓋了java每個(gè)地方, 下面就分別以鎖修飾的位置:變量,方法和類分別介紹。
final修飾成員變量public class FinalExample { //聲明變量的時(shí)候,就進(jìn)行初始化
private final int num=6; //類變量必須要在靜態(tài)初始化塊中指定初始值或者聲明該類變量時(shí)指定初始值
// private final String str; //編譯錯(cuò)誤:因?yàn)榉庆o態(tài)變量不可以在靜態(tài)初始化快中賦初值
private final static String name; private final double score; private final char ch; //private final char ch2;//編譯錯(cuò)誤:TODO:因?yàn)闆]有在構(gòu)造器、初始化代碼塊和聲明時(shí)賦值
{ //實(shí)例變量在初始化代碼塊賦初值
ch=a;
}
static {
name="aaaaa";
}
public FinalExample(){ //num=1;編譯錯(cuò)誤:已經(jīng)賦值后,就不能再修改了
score=90.0;
}
public void ch2(){ //ch2=c;//編譯錯(cuò)誤:實(shí)例方法無法給final變量賦值
}
}
類變量:必須要在靜態(tài)初始化塊中指定初始值或者聲明該類變量時(shí)指定初始值,而且只能在這兩個(gè)地方之一進(jìn)行指定
實(shí)例變量:必要要在非靜態(tài)初始化塊,聲明該實(shí)例變量或者在構(gòu)造器中指定初始值,而且只能在這三個(gè)地方進(jìn)行指定
final修飾局部變量final局部變量由程序員進(jìn)行顯式初始化, 如果final局部變量已經(jīng)進(jìn)行了初始化則后面就不能再次進(jìn)行更改, 如果final變量未進(jìn)行初始化,可以進(jìn)行賦值,當(dāng)且僅有一次賦值,一旦賦值之后再次賦值就會(huì)出錯(cuò)。
public void test(){ final int a=1; //a=2;//編譯錯(cuò)誤:final局部變量已經(jīng)進(jìn)行了初始化則后面就不能再次進(jìn)行更改}
final基本數(shù)據(jù)類型 VS final引用數(shù)據(jù)類型
如果final修飾的是一個(gè)基本數(shù)據(jù)類型的數(shù)據(jù),一旦賦值后就不能再次更改, 那么,如果final是引用數(shù)據(jù)類型了?這個(gè)引用的對象能夠改變嗎?
public class FinalExample2 { private static class Person { private String name; private int age; public void setName(String name) { this.name = name;
} public String getName() { return name;
} public void setAge(int age) { this.age = age;
} public int getAge() { return age;
} public Person(String name, int age) { this.name=name; this.age = age;
} @Override
public String toString() { StringBuilder res=new StringBuilder();
res.append("[").append("name="+name+",age="+age).append("]"); return res.toString();
}
} private static final Person person=new Person("小李子",23); public static void main(String[] args) { System.out.println(person);
person.setAge(24); System.out.println(person);
}
}
輸出結(jié)果:
[name=小李子,age=23] [name=小李子,age=24]
當(dāng)我們對final修飾的引用數(shù)據(jù)類型變量person的屬性改成24,是可以成功操作的。 通過這個(gè)實(shí)驗(yàn)我們就可以看出來當(dāng)final修飾基本數(shù)據(jù)類型變量時(shí),不能對基本數(shù)據(jù)類型變量重新賦值, 因此基本數(shù)據(jù)類型變量不能被改變。 而對于引用類型變量而言,它僅僅保存的是一個(gè)引用,final只保證這個(gè)引用類型變量所引用的地址不會(huì)發(fā)生改變, 即一直引用這個(gè)對象,但這個(gè)對象屬性是可以改變的。
宏變量
利用final變量的不可更改性,在滿足以下三個(gè)條件時(shí),該變量就會(huì)成為一個(gè)“宏變量”,即是一個(gè)常量。
使用final修飾符修飾
在定義該final變量時(shí)就指定了初始值;
該初始值在編譯時(shí)就能夠唯一確定
注意:當(dāng)程序中其他地方使用該宏變量的地方,編譯器會(huì)直接替換成該變量的值。
final修飾方法重寫(Override)
被final修飾的方法不能夠被子類所重寫。 比如在Object中,getClass()方法就是final的,我們就不能重寫該方法, 但是hashCode()方法就不是被final所修飾的,我們就可以重寫hashCode()方法。
重載(Overload)
被final修飾的方法是可以重載的
final修飾類當(dāng)一個(gè)類被final修飾時(shí),該類是不能被子類繼承的。 子類繼承往往可以重寫父類的方法和改變父類屬性,會(huì)帶來一定的安全隱患, 因此,當(dāng)一個(gè)類不希望被繼承時(shí)就可以使用final修飾。
不可變類
final經(jīng)常會(huì)被用作不變類上。我們先來看看什么是不可變類:
使用private和final修飾符來修飾該類的成員變量
提供帶參的構(gòu)造器用于初始化類的成員變量
僅為該類的成員變量提供getter方法,不提供setter方法,因?yàn)槠胀ǚ椒o法修改fina修飾的成員變量
如果有必要就重寫Object類的hashCode()和equals()方法,應(yīng)該保證用equals()判斷相同的兩個(gè)對象其Hashcode值也是相等的。
JDK中提供的八個(gè)包裝類和String類都是不可變類。
final域重排序規(guī)則 final為基本類型public class FinalDemo { private int a; //普通域
private final int b; //final域-->int基本類型
private static FinalDemo finalDemo;//引用類型,但不是final修飾的
public FinalDemo() {
a = 1; // 1. 寫普通域
b = 2; // 2. 寫final域
} public static void writer() {
finalDemo = new FinalDemo();
} public static void reader() { FinalDemo demo = finalDemo; // 3.讀對象引用
int a = demo.a; //4.讀普通域
int b = demo.b; //5.讀final域
}
}
假設(shè)線程A在執(zhí)行writer()方法,線程B執(zhí)行reader()方法。
寫final域重排序規(guī)則
寫final域的重排序規(guī)則:禁止對final域的寫重排序到構(gòu)造函數(shù)之外,這個(gè)規(guī)則的實(shí)現(xiàn)主要包含了兩個(gè)方面:
JMM禁止編譯器把final域的寫重排序到構(gòu)造函數(shù)之外
編譯器會(huì)在final域?qū)懼螅瑯?gòu)造函數(shù)return之前,插入一個(gè)StoreStore屏障。 這個(gè)屏障可以禁止處理器把final域的寫重排序到構(gòu)造函數(shù)之外。 (參見 StoreStore Barriers的說明:在Store1;Store2之間插入StoreStore,確保Store1對 其他處理器可見(刷新內(nèi)存)先于Store2及所有后續(xù)存儲(chǔ)指令的存儲(chǔ))
writer方法中,實(shí)際上做了兩件事:
構(gòu)造了一個(gè)FinalDemo對象
把這個(gè)對象賦值給成員變量finalDemo
可能的執(zhí)行時(shí)序圖如下:
a,b之間沒有數(shù)據(jù)依賴性,普通域(普通變量)a可能會(huì)被重排序到構(gòu)造函數(shù)之外, 線程B就有可能讀到的是普通變量a初始化之前的值(零值),這樣就可能出現(xiàn)錯(cuò)誤。
final域變量b,根據(jù)重排序規(guī)則,會(huì)禁止final修飾的變量b重排序到構(gòu)造函數(shù)之外,從而b能夠正確賦值, 線程B就能夠讀到final變量初始化后的值。
因此,寫final域的重排序規(guī)則可以確保:在對象引用為任意線程可見之前,對象的final域已經(jīng)被正確初始化過了。 普通域不具有這個(gè)保障,比如在上例,線程B有可能就是一個(gè)未正確初始化的對象finalDemo。
讀final域重排序規(guī)則
讀final域重排序規(guī)則:在一個(gè)線程中,初次讀對象引用和初次讀該對象包含的final域,JMM會(huì)禁止這兩個(gè)操作的重排序。 (注意,這個(gè)規(guī)則僅僅是針對處理器), 處理器會(huì)在讀final域操作的前面插入一個(gè)LoadLoad屏障。 實(shí)際上,讀對象的引用和讀該對象的final域存在間接依賴性,一般處理器不會(huì)重排序這兩個(gè)操作。 但是有一些處理器會(huì)重排序,因此,這條禁止重排序規(guī)則就是針對這些處理器而設(shè)定的。
read方法主要包含了三個(gè)操作:
初次讀引用變量finalDemo
初次讀引用變量finalDemo的普通域a
初次讀引用變量finalDemo的final域b
假設(shè)線程A寫過程沒有重排序,那么線程A和線程B有一種的可能執(zhí)行時(shí)序如下:
讀對象的普通域被重排序到了讀對象引用的前面就會(huì)出現(xiàn)線程B還未讀到對象引用就在讀取該對象的普通域變量,這顯然是錯(cuò)誤的操作。
final域的讀操作就“限定”了在讀final域變量前已經(jīng)讀到了該對象的引用,從而就可以避免這種情況。
因此,讀final域的重排序規(guī)則可以確保:在讀一個(gè)對象的final域之前,一定會(huì)先讀這個(gè)包含這個(gè)final域的對象的引用。
final為引用類型public class FinalReferenceDemo { final int[] arrays; //arrays是引用類型
private FinalReferenceDemo finalReferenceDemo; public FinalReferenceDemo() {
arrays = new int[1]; //1
arrays[0] = 1; //2
} public void writerOne() {
finalReferenceDemo = new FinalReferenceDemo(); //3
} public void writerTwo() {
arrays[0] = 2; //4
} public void reader() { if (finalReferenceDemo != null) { //5
int temp = finalReferenceDemo.arrays[0]; //6
}
}
}
對final修飾的對象的成員域進(jìn)行寫操作
針對引用數(shù)據(jù)類型,final域?qū)戓槍幾g器和處理器重排序增加了這樣的約束: 在構(gòu)造函數(shù)內(nèi)對一個(gè)final修飾的對象的成員域的寫入,與隨后在構(gòu)造函數(shù)之外把這個(gè)被構(gòu)造的對象的引用賦給一個(gè)引用變量,這兩個(gè)操作是不能被重排序的。 注意這里的是“增加”也就說前面對final基本數(shù)據(jù)類型的重排序規(guī)則在這里還是使用。
線程線程A執(zhí)行wirterOne方法,執(zhí)行完后線程B執(zhí)行writerTwo方法,線程C執(zhí)行reader方法。 下圖就以這種執(zhí)行時(shí)序出現(xiàn)的一種情況來討論:
對final域的寫禁止重排序到構(gòu)造方法外,因此1和3不能被重排序。 由于一個(gè)final域的引用對象的成員域?qū)懭氩荒芘c在構(gòu)造函數(shù)之外將這個(gè)被構(gòu)造出來的對象賦給引用變量重排序, 因此2和3不能重排序。
對final修飾的對象的成員域進(jìn)行讀操作
JMM可以確保線程C至少能看到寫線程A對final引用的對象的成員域的寫入,即能看到arrays[0] = 1,而 寫線程B對數(shù)組元素的寫入可能看到可能看不到。 JMM不保證線程B的寫入對線程C可見,線程B和線程C之間存在數(shù)據(jù)競爭,此時(shí)的結(jié)果是不可預(yù)知的。 如果想要可見,可使用鎖或者volatile。
final重排序的總結(jié)
final寫 | final域讀 | |
---|---|---|
基本數(shù)據(jù)類型 | 禁止final域?qū)懪c構(gòu)造方法重排序,即禁止final域?qū)懼嘏判虻綐?gòu)造方法之外,從而保證該對象對所有線程可見時(shí),該對象的final域全部已經(jīng)初始化過 | 禁止初次讀對象的引用與讀該對象包含的final域的重排序,保證了在讀一個(gè)對象的final域之前,一定會(huì)先讀這個(gè)包含這個(gè)final域的對象的引用 |
引用數(shù)據(jù)類型 | 額外增加約束:構(gòu)造函數(shù)內(nèi)對一個(gè)final修飾的對象的成員域的寫入,與隨后在構(gòu)造函數(shù)之外把這個(gè)被構(gòu)造的對象的引用賦給一個(gè)引用變量,這兩個(gè)操作是不能被重排序的 |
對象溢出:一種錯(cuò)誤的發(fā)布,當(dāng)一個(gè)對象還沒有構(gòu)造完成時(shí),就使它被其他線程所見。
/*** 對象溢出示例*/public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
void doSomething(Event e) {
}
}
這將導(dǎo)致this逸出,所謂逸出,就是在不該發(fā)布的時(shí)候發(fā)布了一個(gè)引用。
在這個(gè)例子里面,當(dāng)我們實(shí)例化ThisEscape對象時(shí),會(huì)調(diào)用source的registerListener方法, 這時(shí)便啟動(dòng)了一個(gè)線程,而且這個(gè)線程持有了ThisEscape對象(調(diào)用了對象的doSomething方法), 但此時(shí)ThisEscape對象卻沒有實(shí)例化完成(還沒有返回一個(gè)引用),所以我們說, 此時(shí)造成了一個(gè)this引用逸出,即還沒有完成的實(shí)例化ThisEscape對象的動(dòng)作,卻已經(jīng)暴露了對象的引用。
正確構(gòu)造過程:
public class SafeListener {
private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
void doSomething(Event e) {
}
當(dāng)構(gòu)造好了SafeListener對象(通過構(gòu)造器構(gòu)造)之后, 我們才啟動(dòng)了監(jiān)聽線程,也就確保了SafeListener對象是構(gòu)造完成之后再使用的SafeListener對象。
結(jié)論:
只有當(dāng)構(gòu)造函數(shù)返回時(shí),this引用才應(yīng)該從線程中逸出。
構(gòu)造函數(shù)可以將this引用保存到某個(gè)地方,只要其他線程不會(huì)在構(gòu)造函數(shù)完成之前使用它。
final的實(shí)現(xiàn)原理寫final域會(huì)要求編譯器在final域?qū)懼?,?gòu)造函數(shù)返回前插入一個(gè)StoreStore屏障。
讀final域的重排序規(guī)則會(huì)要求編譯器在讀final域的操作前插入一個(gè)LoadLoad屏障。
如果以X86處理為例,X86不會(huì)對寫-寫重排序,所以StoreStore屏障可以省略。 由于不會(huì)對有間接依賴性的操作重排序,所以在X86處理器中,讀final域需要的LoadLoad屏障也會(huì)被省略掉。 也就是說,以X86為例的話,對final域的讀/寫的內(nèi)存屏障都會(huì)被省略!具體是否插入還是得看是什么處理器。
注意:
上面對final域?qū)懼嘏判蛞?guī)則可以確保我們在使用一個(gè)對象引用的時(shí)候該對象的final域已經(jīng)在構(gòu)造函數(shù)被初始化過了。 但是這里其實(shí)是有一個(gè)前提條件: 在構(gòu)造函數(shù),不能讓這個(gè)被構(gòu)造的對象被其他線程可見,也就是說該對象引用不能在構(gòu)造函數(shù)中“溢出”。
public class FinalReferenceEscapeDemo { private final int a; private FinalReferenceEscapeDemo referenceDemo; public FinalReferenceEscapeDemo() {
a = 1; //1
referenceDemo = this; //2
} public void writer() { new FinalReferenceEscapeDemo();
} public void reader() { if (referenceDemo != null) { //3
int temp = referenceDemo.a; //4
}
}
}
假設(shè)一個(gè)線程A執(zhí)行writer方法另一個(gè)線程執(zhí)行reader方法。因?yàn)闃?gòu)造函數(shù)中操作1和2之間沒有數(shù)據(jù)依賴性,1和2可以重排序,先執(zhí)行了2,這個(gè)時(shí)候引用對象referenceDemo是個(gè)沒有完全初始化的對象,而當(dāng)線程B去讀取該對象時(shí)就會(huì)出錯(cuò)。盡管依然滿足了final域?qū)懼嘏判蛞?guī)則:在引用對象對所有線程可見時(shí),其final域已經(jīng)完全初始化成功。 但是,引用對象“this”逸出,該代碼依然存在線程安全的問題。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/7116.html
摘要:基礎(chǔ)知識(shí)復(fù)習(xí)后端掘金的作用表示靜態(tài)修飾符,使用修飾的變量,在中分配內(nèi)存后一直存在,直到程序退出才釋放空間。將對象編碼為字節(jié)流稱之為序列化,反之將字節(jié)流重建成對象稱之為反序列化。 Java 學(xué)習(xí)過程|完整思維導(dǎo)圖 - 后端 - 掘金JVM 1. 內(nèi)存模型( 內(nèi)存分為幾部分? 堆溢出、棧溢出原因及實(shí)例?線上如何排查?) 2. 類加載機(jī)制 3. 垃圾回收 Java基礎(chǔ) 什么是接口?什么是抽象...
摘要:寫域重排序規(guī)則寫域的重排序規(guī)則禁止對域的寫重排序到構(gòu)造函數(shù)之外,這個(gè)規(guī)則的實(shí)現(xiàn)主要包含了兩個(gè)方面禁止編譯器把域的寫重排序到構(gòu)造函數(shù)之外編譯器會(huì)在域?qū)懼?,?gòu)造函數(shù)之前,插入一個(gè)屏障。結(jié)論只有當(dāng)構(gòu)造函數(shù)返回時(shí),引用才應(yīng)該從線程中逸出。final關(guān)鍵字final的簡介final可以修飾變量,方法和類,用于表示所修飾的內(nèi)容一旦賦值之后就不會(huì)再被改變,比如String類就是一個(gè)final類型的類。f...
摘要:寫域重排序規(guī)則寫域的重排序規(guī)則禁止對域的寫重排序到構(gòu)造函數(shù)之外,這個(gè)規(guī)則的實(shí)現(xiàn)主要包含了兩個(gè)方面禁止編譯器把域的寫重排序到構(gòu)造函數(shù)之外編譯器會(huì)在域?qū)懼?,?gòu)造函數(shù)之前,插入一個(gè)屏障。結(jié)論只有當(dāng)構(gòu)造函數(shù)返回時(shí),引用才應(yīng)該從線程中逸出。final關(guān)鍵字final的簡介final可以修飾變量,方法和類,用于表示所修飾的內(nèi)容一旦賦值之后就不會(huì)再被改變,比如String類就是一個(gè)final類型的類。f...
摘要:基礎(chǔ)問題的的性能及原理之區(qū)別詳解備忘筆記深入理解流水線抽象關(guān)鍵字修飾符知識(shí)點(diǎn)總結(jié)必看篇中的關(guān)鍵字解析回調(diào)機(jī)制解讀抽象類與三大特征時(shí)間和時(shí)間戳的相互轉(zhuǎn)換為什么要使用內(nèi)部類對象鎖和類鎖的區(qū)別,,優(yōu)缺點(diǎn)及比較提高篇八詳解內(nèi)部類單例模式和 Java基礎(chǔ)問題 String的+的性能及原理 java之yield(),sleep(),wait()區(qū)別詳解-備忘筆記 深入理解Java Stream流水...
閱讀 769·2023-04-25 19:43
閱讀 4022·2021-11-30 14:52
閱讀 3855·2021-11-30 14:52
閱讀 3909·2021-11-29 11:00
閱讀 3839·2021-11-29 11:00
閱讀 3949·2021-11-29 11:00
閱讀 3613·2021-11-29 11:00
閱讀 6310·2021-11-29 11:00