final關鍵字及其內存語義

 

  1、 final  

  學習的要義:多問問出現的背景,可以解決什麼問題,如何使用,對比別的方案有什麼優點,是否有改進的地方?
html

 1.概述

    final關鍵字可以告訴編譯器一塊數據是恆定不變的,thinking in java 中提到的不想被改變的兩種理由:設計和效率。java

 2.用法 :熊貓人都知道.....

 

    2.1 修飾變量

    做用:表示該變量不能夠被再次賦值修改;編程

 變量主要分爲基本數據類型和引用;併發

    ①若是修飾的是基本數據類型,就表示該變量的值永遠不會改變,若是你試圖改變他,例以下面的代碼則會出現編譯器警告:oracle

  Cannot assign a value to final variable ide

    private final int par = 0;
public void modifyFinalTest() { par = 4; }

 另外須要注意的是java容許用final修飾某個未進行初始化複製的變量,這叫作「空白final」,可是編譯器必定可以確保空白final在使用前被初始化,否則報錯給你看   函數

例以下圖中的 13 行代碼,以及17行的變量未進行初始化都沒法經過編譯。學習

        

 

  ②修飾引用spa

   某個被final修飾的引用一旦被初始化指向一個對象後,就不能夠將它改成指向另外一個對象,須要注意的是該對象自己所屬的類行爲是不會受到限制的。線程

 例以下面的list,初始化後若是想要修改其引用則沒法經過編譯,可是strList對象對於的List類的行爲是不會受到改變的,如add方法

 


                                             

    

 

這裏只是限制了引用不可變,還有更狠的操做,JDK 9 以後出現的List.of 方法建立的「不可變list」連list裏面的「內容」都不能變了。

下面是JDK 9 中的「新番「:

           

 

                                             

    

 

 

 

 

List.of 方法建立的list是「不可變」的,若是試圖去修改不可變list中的內容則會拋出異常;

另外你們實際開發中常見的問題,匿名內部類訪問外部類中的局部變量時,爲何要將該變量聲明爲final類型的?(JDK8 以後不須要手動添加final關鍵字了)

緣由:匿名內部類對象的生命週期比外部類中的局部變量長;

  局部變量的生命週期:當有方法調用並使用到該變量時,變量入棧,方法執行結束後,出棧,變量就銷亡了;

  對象的生命週期:當沒有引用指向這個對象,GC會在某個時候將其回收,也就是銷燬了。

問題:成員方法執行完了,局部變量銷燬了,可是對象還仍然存活(沒有被GC),這時候對象要去引用該局部變量就引用不到了。

解決方法:java中的內部類訪問外部變量時,必須將該變量聲明爲final,而且inner class會copy一份該變量,而不是直接去使用該局部變量,這樣就能夠防止數據不一  致的問題了。

java的改進:JDK8 後,若是有內部類訪問局部變量,java會自動將該變量修飾成final類型的,因此咱們不須要再去手動添加該關鍵字。

 

     2.2 修飾方法

 表示該方法不能夠被重寫(override);比較簡單就不展開了。

 

     2.3 修飾類;

 表示該類不能夠被繼承擴展;

 

這些相信你們都已經掌握了,最關鍵的是final關鍵字修飾的字段在內存方面有什麼影響?

 

  3.final的內存語義

Oracle官方對於final的說明: https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.5   

注:還能夠去看看《Java併發編程的藝術》 P55 ;對這個官方文檔進行了很好的說明及補充;

總結以下:

Java內存模型規定了

第一條,對於final變量的初始化重排序規則:   final 關鍵字修飾的變量初始化的代碼 不能重排序到構造函數結束以後;           

第二條,對於final變量的讀取重排序規則:       初次讀對象引用與初次讀該對象包含的final 域,JMM禁止處理器重排序這兩個操做。而這兩個操做間存在依賴關係,通常編譯器遵照間接依賴關係,不會對其進行重排序。大多數處理器也會遵照間接依賴原則,不會對其重排序。(少數傻吊會對其重排序。。。後面會講到)

 

首先第一條是什麼意思呢?來看看官方的例子

場景: 這個例子中定義了一個final 變量 x 和一個普通變量 y ;在構造函數中賦值。  此時有寫和讀兩個線程開始分別調用writer() 和reader()方法;

最後的結果你猜猜有多少種可能呢?

狀況 結果
正常狀況 i = 3;  y =4
非正常狀況 i = 3; y = 0

爲何會出現這種非正常狀況呢?

由於我前面說到的是final關鍵字修飾的變量才能確保不會被重排序到構造函數以後。 普通變量就沒這待遇了。

因此通過編譯器和處理器重排序後的代碼的非正常狀況就是這樣的:

                                                        

寫線程 讀線程
1. 構造函數開始執行;  
2. 構造函數中給 final 變量賦值爲3;  
3. 構造函數執行結束;  
4.將構造對象的引用賦值給引用變量f  
  1.讀取初始化完成的對象
  2.讀取該對象中的普通變量 y (有問題)
5.給普通變量y 賦值 爲4  

結論: 對於空白final 變量在構造函數中的初始化 代碼 不能夠重排序到 構造函數以後,必須在構造函數裏面完成初始化,  普通變量在不改變單線程運行結果的狀況下的初始化能夠重排序到構造函數以後。

 

第二條啥意思呢?上面講到有少數「傻吊」處理器會對 讀對象和 讀對象中的變量操做進行重排序。

場景:

有一個寫線程和一個讀線程;

讀線程的操做:

正常讀取 重排序後的讀取
1.讀取對象obj 1. 讀取obj中的普通變量(問題)
2.讀取obj中的普通變量 2.讀取對象obj
3.讀取obj中的final變量 3.讀取對象obj中的final變量
   

重排序後的讀取問題在於  讀取普通變量 時該普通域還未被初始化,因此讀取到的數據時不對的,可是JMM對於final變量讀取限制了必須先要讀取包含它的對象,而後再去讀取該final變量;

 總結: 其實一個小小的final關鍵字包含的內容是很是多的,這背後爲了數據一致性考量的大佬們,在編譯器和處理器層面制定了各類規則,因此咱們才能用的方便,喜歡刨根問底的朋友能夠參考下面的文檔,最後呢,但願你們多多交流哈,若是有什麼問題請幫忙指出!多謝!

 

1. https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4       (oracle官方對於final內存語義的說明) 

2. 《併發編程的藝術》  P55

3. Java編程思想,fianl關鍵字

4.https://en.wikipedia.org/wiki/Final_(Java)#Final_and_inner_classes

 

相關文章
相關標籤/搜索