本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫
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變量的成員變量,後調用該對象引用。