java中的final和volatile詳解

  相比synchronized,final和volatile也是常用的關鍵字,下面聊一聊這兩個關鍵字的使用和實現html

1.使用java

  final使用:緩存

  • 修飾類表示該類爲終態類,沒法被繼承
  • 修飾方法表示該方法沒法重寫,編譯器能夠內聯編譯
  • 修飾對象表示該對象引用一旦初始化後,沒法被修改
  • 將參數傳遞到匿名內部類中,參數須要聲明爲final,其實外部類對與匿名內部類來講就是一個閉包,而java在匿名內部類中拷貝了一份,沒有實現引用同步,因此要求參數不可變(參考:https://www.zhihu.com/question/21395848)

例子:閉包

 

class FinalFieldExample {
  final int x;
  int y;
  static FinalFieldExample f;
  public FinalFieldExample() {
    x = 3;
    y = 4;
  }

  static void writer() {
    f = new FinalFieldExample();
  }

  static void reader() {
    if (f != null) {
      int i = f.x;
      int j = f.y;
    }
  }
}

 

  調用reader方法的線程保證了當f不爲null時,x的值必定能夠讀取到,由於x聲明爲了final,而y則不必定併發

 

  volatile使用:app

  •  通常修飾對象
  • 包含兩個含義:可見性,禁止指令重排

JSR133 FAQ中例子1:函數

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }

  public void reader() {
    if (v == true) {
      //uses x - guaranteed to see 42.
    }
  }
}

  上邊這個例子中,一個線程調用writer方法,一個線程調用reader發放,當先調用writer方法,後調用reader方法時,因爲對象v聲明爲volatile,具備可見性,也就是一個線程的修改會當即在另外一個線程中體現出來,所以reader方法中斷定會爲true,若是進入該分支後,保證x的值必定爲42,由於volatile保證了禁止指令重排,因此writer中第一個賦值必定會在第二個賦值前執行。優化

JSR133 FAQ中例子2:this

private volatile static Something instance = null;

public Something getInstance() {
  if (instance == null) {
    synchronized (this) {
      if (instance == null)
        instance = new Something();
    }
  }
  return instance;
}

  以上是一個典型double-check locking例子,instance聲明爲volatile保證了構造Something對象的指令和賦值給instance的指令不會重排,這樣的話當其餘線程拿到instance的引用不爲null時,instance已經初始化完畢了spa

2.規則和原理 

   在解釋下面規則原理以前仍是要在說明一下,編譯器和處理器爲了優化程序執行的速度,會對指令進行重排序,下面經過一個例子來講明:

public class PossibleReordering {
static int x = 0, y = 0;
static int a = 0, b = 0;

public static void main(String[] args) throws InterruptedException {
    Thread one = new Thread(new Runnable() {
        public void run() {
            a = 1; //1
            x = b; //2
        }
    });

    Thread other = new Thread(new Runnable() {
        public void run() {
            b = 1; //3
            y = a; //4
        }
    });
    one.start();other.start();
    one.join();other.join();
    System.out.println(「(」 + x + 「,」 + y + 「)」);
}

  通常可能認爲,這個代碼的執行結果可能有三種,分別是(1,0),(0,1),(1,1)(雖然這種狀況沒有跑出來)這三種狀況,可是當連續執行10000屢次的時候,發現竟然有(0,0)這種狀況,實際上這是由於指令在執行的時候發生了重排序,也就是說編譯器和處理器會根據實際狀況優化代碼執行的順序。指令重排序是以as if serial優化的,因此只要保證在單線程下,最後的執行結果一致便可。上面這個例子就是發生了重排序,若是步驟1和步驟2發生重排序,致使實際執行順序爲2->3->4->1,那麼就會出現(0,0)

 

  JSR133(JMM)中對final域在重排序方面進行了約束,以保證final的正確使用

  final規則

  當final域爲對象的時候,編譯器和處理器須要遵循這兩個重排序原則:

  1. 在構造函數中對一個final對象的寫入,與後面的把構造對象的引用賦值給引用對象,這兩個操做不得重排序
  2. 初次讀取包含一個final對象的引用,和初次讀取這個final對象,這兩個操做不得重排序

  看下面的例子:

public class FinalExample {
    int i;                            //普通變量
    final int j;                      //final變量
    static FinalExample obj;

    public void FinalExample () {     //構造函數
        i = 1;                        //寫普通域
        j = 2;                        //寫final域
    }

    public static void writer () {    //寫線程A執行
        obj = new FinalExample ();
    }

    public static void reader () {       //讀線程B執行
        FinalExample object = obj;       //讀對象引用
        int a = object.i;                //讀普通域
        int b = object.j;                //讀final域
    }
}

  第一條規則實際上表達的是對final域的寫入不能夠重排序到構造函數外,這一條本質上包含了下面兩條規則:

  1. 針對編譯器,編譯器不會將構造函數中final域對寫入重排序到構造函數外;
  2. 針對處理器,編譯器會在構造函數返回結束前,加入一個storestore屏障(後續再詳細解釋),保證處理器不會將final域的寫入重排序到構造函數外

  所以當線程B執行的時候(不考慮讀取時候的重排序),當讀取object引用時,對象內到final域已經初始化好了,能夠正常讀取,可是普通域可能沒有初始化好

  第二條規則一樣也須要在編譯器和處理器層面去保證:

  1. 針對編譯器,因爲讀對象的引用和對象引用中的final域,這兩個操做存在關聯關係,因此編譯器不會重排序
  2. 針對處理器,編譯器會在讀取對象引用中的final域前,插入一個loadload屏障,保證讀對象的引用和對象引用中的final域這兩個操做不會重排序

  所以當線程B執行的時候,讀取對象引用和讀取對象中的普通域可能發生重排,而讀取對象引用和對象中的final域不會,這樣經過和第一條結合時候,對於final域,併發狀況下,能夠保證final域的正常讀取

 

  上面看到對final域對對象實際上是基礎類型,若是是引用類型呢

public class FinalReferenceExample {
final int[] intArray;                     //final是引用類型
static FinalReferenceExample obj;

public FinalReferenceExample () {        //構造函數
    intArray = new int[1];              //1
    intArray[0] = 1;                   //2
}

public static void writerOne () {          //寫線程A執行
    obj = new FinalReferenceExample ();  //3
}

public static void writerTwo () {          //寫線程B執行
    obj.intArray[0] = 2;                 //4
}

public static void reader () {              //讀線程C執行
    if (obj != null) {                    //5
        int temp1 = obj.intArray[0];       //6
    }
}
}

  對於final域爲引用對象的狀況,編譯器和處理器有下面對重排序限制:

  1. 在構造函數裏對final域的引用對象中的成員的寫入,和構造對象的引用的賦值操做,這兩個操做不得重排序

  咱們先執行線程A,再執行線程B、最後執行線程C,因爲重排序的限制,步驟3與步驟1,步驟3與步驟2不可重排序,而步驟1和步驟2存在關聯關係,所以線程C執行的時候能夠正常讀取到final域引用對象的成員值。而線程B的修改是否能夠在線程C中讀取到則不必定了,須要在線程B、C之間須要使用同步原語

  逃逸

  上面咱們經過例子說明了一個問題,構造函數中的final域引用不可逃脫出構造函數,那麼若是經過其餘方式將構造對象暴露出去呢,請看下面這個例子:

public class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;

public FinalReferenceEscapeExample () {
    i = 1;                              //1寫final域
    obj = this;                          //2 this引用在此「逸出」
}

public static void writer() {
    new FinalReferenceEscapeExample ();
}

public static void reader {
    if (obj != null) {                     //3
        int temp = obj.i;                 //4
    }
}
}

   上面這個例子中,final域的重排序限制沒法限制步驟1和步驟2的重排序,那麼就有可能出現逃逸現象,當reader線程執行時,可能沒法正常訪問到構造對象中final域初始化後的值

  volatile規則

  爲了達到java跨平臺的語言特性,須要將內存從新抽象,這樣就誕生了jsr133,jsr133描述了java內存模型,屏蔽了底層實現的差別,保證相同的代碼在不一樣平臺上具備相同的表現。根據java內存模型(java memory model,簡稱JMM)的規定,能夠簡化爲幾個happen-before原則,happen-before先後兩個操做不可重排序而且前者對後者內存可見:

  • 程序次序法則:線程中的每一個動做A都happens-before於該線程中的每個動做B,其中,在程序中,全部的動做B都能出如今A以後。
  • 監視器鎖法則:對一個監視器鎖(monitor)的解鎖 happens-before於每個後續對同一監視器鎖的加鎖,monitor爲同步原語的實現方式。
  • volatile變量法則:對volatile域的寫入操做happens-before於每個後續對同一個域的讀寫操做,寫入操做寫入內存,讀取操做緩存失效讀取內存,保證可見性。
  • 線程啓動法則:在一個線程裏,對Thread.start的調用會happens-before於每一個啓動線程的動做。
  • 線程終結法則:線程中的任何動做都happens-before於其餘線程檢測到這個線程已經終結、或者從Thread.join調用中成功返回,或Thread.isAlive返回false。
  • 中斷法則:一個線程調用另外一個線程的interrupt happens-before於被中斷的線程發現中斷。
  • 終結法則:一個對象的構造函數的結束happens-before於這個對象finalizer的開始。
  • 傳遞性:若是A happens-before於B,且B happens-before於C,則A happens-before於C

  happen-before原則是對java內存模型對近似描述,更嚴謹的java模型定義參考jsr133。jsr133對volatile語意進行了擴展,特別是關於重排序這方面,具體限制以下:

重排序示意表

  第二項操做指的是第一項操做後面的全部操做,例如,普通的讀寫操做不可與以後的volatile變量的寫操做重排序,參考上面volatile例子,留白的單元格表示在保證java語意不變的狀況下能夠重排序,例如,java語意不容許對同一個對象的讀寫重排序,可是對不一樣對對象的讀寫能夠

  內存屏障

  內存屏障(memory barrier,也稱做內存欄柵)是一種CPU指令,用於控制指令重排序和解決可見性問題

  內存屏障能夠被分爲如下幾種類型

  • LoadLoad屏障:對於這樣的語句Load1; LoadLoad; Load2,在Load2及後續讀取操做要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。
  • StoreStore屏障:對於這樣的語句Store1; StoreStore; Store2,在Store2及後續寫入操做執行前,保證Store1的寫入操做對其它處理器可見。
  • LoadStore屏障:對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操做被刷出前,保證Load1要讀取的數據被讀取完畢。
  • StoreLoad屏障:對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續全部讀取操做執行前,保證Store1的寫入對全部處理器可見。它的開銷是四種屏障中最大的。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種內存屏障的功能。

  上面的重排序規則能夠經過內存屏障指令實現:

內存屏障示意表

  總的來講,內存屏障指令提供了兩個方面的功能:

  1. 內存屏障指令先後指令不可重排序,具體重排序限制根據四種內存屏障指令不一樣而不一樣,具體含義參考上面的表格
  2. 若是是storeload或者storestore指令,要求volatile對象的寫操做寫入內存中,同時會致使其餘CPU中的緩存行失效

  第一條,咱們已經在上面闡明瞭,對於第二條功能是經過緩存一致性協議達到,緩存一致性協議在單機多核的狀況下是經過硬件實現。最爲出名的緩存一致性協議是Intel的MESI。

 

三、總結 

  final和volatile語意在jsr133中作了相應擴展,保證了其語意的正確性。正確理解其使用規則和編譯器和處理器實現原理對咱們平常工做有意義,不論是final仍是volatile底層都依賴內存屏障技術,內存屏障技術(指令)最重要的功能就是對指令重排序對限制,對於volatile對語意中可見性語意,經過內存屏障技術和緩存一致性協議實現。

    

參考:

http://www.infoq.com/cn/articles/java-memory-model-6

http://www.cs.umd.edu/users/pugh/java/memoryModel/jsr-133-faq.html

https://tech.meituan.com/java-memory-reordering.html

http://www.cnblogs.com/dolphin0520/p/3920373.html

相關文章
相關標籤/搜索