個人博客請訪問 http://www.x0100.top
final在Java中是一個保留的關鍵字,可以修飾變量、方法和類。那麼fianl在併發編程中有什麼作用呢?本文就在對final常見應用總結基礎上,講解final併發編程中的應用。
1. final基礎應用
final變量
final變量只能被賦值一次,賦值後值不再改變。(final要求地址值不能改變)
當final修飾一個基本數據類型時,表示該基本數據類型的值一旦在初始化後便不能發生變化;
如果final修飾一個引用類型時,則在對其初始化之後便不能再讓其指向其他對象了,但該引用所指向的對象的內容是可以發生變化的。
本質上是一回事,因爲引用的值是一個地址,final要求地址值不發生變化。
當final修飾一個基本數據類型時,表示該基本數據類型的值一旦在初始化後便不能發生變化;如果final修飾一個引用類型時,則在對其初始化之後便不能再讓其指向其他對象了,但**該引用所指向的對象的內容是可以發生變化的**。本質上是一回事,因爲引用的值是一個地址,final要求地址值不發生變化。
final成員變量:兩種初始化方式,一種是在變量聲明的時候初始化;第二種是在聲明變量的時候不賦初值,但是要在這個變量所在的類的所有的構造函數中對這個變量賦初值。
final方法
final修飾的方法在編譯階段被靜態綁定(static binding),不能被重寫。
final方法比非final方法要快,因爲在編譯的時候已經靜態綁定了,不需要在運行時再動態綁定。(注:類的private方法會隱式地被指定爲final方法)
final類
final修飾的類不能被繼承。
final類中的成員變量可以根據需要設爲final,但是要注意final類中的所有成員方法都會被隱式地指定爲final方法。
在使用final修飾類的時候,要注意謹慎選擇,除非這個類真的在以後不會用來繼承或者出於安全的考慮,儘量不要將類設計爲final類。
關於final的幾個重要知識點
-
final關鍵字可以提高性能,JVM和Java應用都會緩存final變量,JVM會對方法、變量及類進行優化。
-
在匿名類中所有變量都必須是final變量。
-
接口中聲明的所有變量本身是final的。
-
final和abstract這兩個關鍵字是反相關的,final類就不可能是abstract的。
-
按照Java代碼慣例,final變量就是常量,而且通常常量名要大寫。
-
final變量可以安全的在多線程環境下進行共享,而不需要額外的同步開銷。
2. 併發編程中的final
2.1 寫final域
在構造函數內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。
編譯器會在final域的寫之後,插入一個StoreStore屏障,這個屏障可以禁止處理器把final域的寫重排序到構造函數之外。
解釋:保證先寫入對象的final變量,後調用該對象引用。
舉例
public class FinalDemo {
private int a; // 普通域
private final int b; // final域
private static FinalDemo finalDemo;
public FinalDemo() {
a = 1; // ①寫普通域
b = 2; // ②寫final域
}
public static void writer() {
// 兩個操作:
// 1)構造一個FinalExample類型的對象,①寫普通域a=1,②寫final域b=2
// 2)③把這個對象的引用賦值給引用變量finalDemo
finalDemo = new FinalDemo();
}
public static void reader() {
FinalDemo demo = finalDemo; // ④讀對象引用
int a = demo.a; // ⑤讀普通域
int b = demo.b; // ⑥讀final域
}
}
假設一個線程A執行writer()方法,隨後另一個線程B執行reader()方法。通過這兩個線程的交互來說明寫final域的規則。下圖是一種可能的執行時序:
寫普通域的操作可以被編譯器重排序到了構造函數,①寫普通域和③把這個對象的引用賦值給引用變量finalDemo重排序,導致讀線程B錯誤的讀取了普通變量a的值。
寫final域的操作不能重排序到了構造函數外,②寫final域和③把這個對象的引用賦值給引用變量finalDemo不能重排序,讀線程B正確的讀取了final變量b的值。
2.2 讀final域
初次讀一個包含final域的對象的引用,與隨後初次讀這個final域,這兩個操作之間不能重排序。
編譯器會在讀final域操作的前面插入一個LoadLoad屏障,這個屏障可以禁止讀對象引用和讀該對象final域重排序。
解釋:先讀對象的引用,後讀該對象的final變量。
舉例:
還是上面那段代碼,假設一個線程A執行writer()方法,隨後另一個線程B執行reader()方法。下圖是一種可能的執行時序:
讀對象的普通域的操作可以被重排序到讀對象引用之前,⑤讀普通域與④讀對象引用重排序,讀普通域a時,a沒有被寫線程A寫入,導致錯誤的讀取。
讀final域的操作不可以被重排序到讀對象引用之前,④讀對象引用和⑥讀final域不能重排序,讀取該final域b時已經被A線程初始化過了,不會有問題。
2.3 final域爲引用類型
對於引用類型,寫final域的重排序規則對編譯器和處理器增加了如下約束:在構造函數內對一個final引用的對象的成員域的寫入,與隨後在構造函數外把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。
解釋:
注意是增加了一條約束,所以以上兩條約束都還生效。
保證先寫入對象的final變量的成員變量,後調用該對象引用。
舉例:
public class FinalReferenceDemo {
final int[] 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
}
}
}
假設首先線程A執行writerOne()方法,執行後線程B執行writerTwo()方法,執行後線程C執行reader()方法。下面是一種可能的線程執行時序:
1是對final域的寫入,2是對這個final域引用的對象的成員域的寫入,3是把被構造的對象的引用賦值給某個引用變量。
由寫final域的重排序規則“寫final域的操作不能重排序到了構造函數外”可知,1和3是不能重排序的。
引用類型final域的重排序規則“final引用的對象的成員域的寫入不能重排序到了構造函數外”,保證了2和3不能重排序。所以線程C至少能看到數組下標0的值爲1。
寫線程B對數組元素的寫入,讀線程C不一定能看到。因爲寫線程B和讀線程C之間存在數據競爭,此時的執行結果不可預知。
總結
final基礎應用
-
final修飾的變量地址值不能改變。
-
final修飾的方法不能被重寫。
-
final修飾的類不能被繼承。
併發編程中final可以禁止特定的重排序。
-
final保證先寫入對象的final變量,後調用該對象引用。
-
final保證先讀對象的引用,後讀該對象的final變量。
-
final保證先寫入對象的final變量的成員變量,後調用該對象引用。