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