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