Java併發編程之final應用

個人博客請訪問 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引用的對象的成員域的寫入,與隨後在構造函數外把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。

解釋:

  1. 注意是增加了一條約束,所以以上兩條約束都還生效。

  2. 保證先寫入對象的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變量的成員變量,後調用該對象引用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章