「原創」Java並發編程系列06|你不知道的final

2020-12-22 酷扯兒

本文轉載自【微信公眾號: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變量的成員變量,後調用該對象引用。

相關焦點

  • 「原創」Java並發編程系列22|倒計時器CountDownLatch
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫並發編程中常遇到這種情況,一個線程需要等待另外多個線程執行後再執行。遇到這種情況你一般怎麼做呢?今天就介紹一種JDk提供的解決方案來優雅的解決這一問題,那就是倒計時器CountDownLatch。
  • 「原創」Java並發編程系列33|深入理解線程池(上)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫並發編程必不可少的線程池,接下來分兩篇文章介紹線程池,本文是第一篇。介紹1.1 使用場景並發編程可以高效利用CPU資源,提升任務執行效率,但是多線程及線程間的切換也伴隨著資源的消耗。當遇到單個任務處理時間比較短,但需要處理的任務數量很大時,線程會頻繁的創建銷毀,大量的時間和資源都會浪費在線程的創建和銷毀上,效率很低。
  • 「原創」Java並發編程系列36|FutureTask
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫線程池源碼中出現了很多Callable、Future、FutureTask等以前沒介紹過的接口,尤其是線程池提交任務時總是把任務封裝成FutureTask,今天就來為大家解惑:
  • [博客更新]Java 中 final 關鍵詞的使用
    上一篇文章我們講了 java 中 static 關鍵字的使用,這裡再將一下 final 關鍵字的使用。final 在 java 中可以看做一個「終結者」,它可以定義類、定義方法和定義變量。哎呀,好像沒別的可說了,就這樣吧,這可能是我寫過的字數最少的一篇文章了 算了,最後加一個示例程序吧,當然是錯誤的程序,包含了以上三種使用方法的錯誤使用報錯信息:package com.example.finalDemo;/** * User: sunriseydy * 2018-4-25 22:04 */finalclassPerson{String name
  • 解讀 java 並發隊列 BlockingQueue
    燈塔君跟大家講:解讀 java 並發隊列 BlockingQueue最近得空,想寫篇文章好好說說 java 線程池問題,我相信很多人都一知半解的,包括我自己在仔仔細細看源碼之前,也有許多的不解,甚至有些地方我一直都沒有理解到位。
  • Collections.sort 你真的會用嗎?那些你所不知道的坑
    但我知道你手頭沒有 Android 7.0 的機子對不對。如果你使用 Android 7.x 或更低版本系統,Collection.sort() 下的調用棧會拋出 UnsupportedOperationException 異常。不支持的操作,這是什麼鬼東西?
  • Java之final關鍵字詳解
    前言針對Java語言中的final關鍵字,想必都不陌生了。本來主要是來對final做關鍵字做一個總結。final關鍵字用法修飾類當用final去修飾一個類的時候,表示這個類不能被繼承。注意:a.被final修飾的類,final類中的成員變量可以根據自己的實際需要設計為fianl。b. final類中的成員方法都會被隱式的指定為final方法。說明:在自己設計一個類的時候,要想好這個類將來是否會被繼承,如果可以被繼承,則該類不能使用fianl修飾,在這裡呢,一般來說工具類我們往往都會設計成為一個fianl類。在JDK中,被設計為final類的有String、System等。
  • 這些 Java 8 官方挖的坑,你踩過幾個?
    往List裡面添加數據竟然提示不支持?日期明明間隔1年卻輸出1天,難不成這是天上人間?1582年神秘消失的10天JDK能否識別?Stream很高大上,List轉Map卻全失敗……這些JDK8官方挖的坑,你踩過幾個?
  • java中static, final, 內部類的具體運用
    final是個修飾符,它可以用來修飾類,類的成員方法,以及變量(成員變量和局部變量)。final的特點:inal修飾類不可以被繼承,但是可以繼承其他類。class Yy {}final class Fue xtendsYy{} //可以繼承Yy 類class Zi extends Fu{}final修飾的方法不可以被覆蓋,但父類中沒有被final修飾方法,子類覆蓋後可以加final。
  • Java並發包源碼學習系列:CLH同步隊列及同步資源獲取與釋放
    CLH隊列的結構我在Java並發包源碼學習系列:AbstractQueuedSynchronizer#同步隊列與Node節點已經粗略地介紹了一下CLH的結構,本篇主要解析該同步隊列的相關操作,因此在這邊再回顧一下:AQS通過內置的FIFO同步雙向隊列來完成資源獲取線程的排隊工作,內部通過節點head【實際上是虛擬節點,真正的第一個線程在head.next的位置】和tail
  • 探索Java中的網絡編程技術
    承蒙關照~探索Java中的網絡編程技術網絡編程就是io技術和網絡技術的結合,網絡模型的定義,只要共用網絡模型就可以兩者連接.網絡模型參考.一座塔有七層,我們需要闖關.第一層物理層->第二層數據鏈路層->第三層網絡層->第四層傳輸層->第五層會話層->第六層表示層->第七層應用層.
  • 《新·福音戰士劇場版: ||》聯名耳機「EVA2020 × final」曝光
    日本專業音響耳機製造商SNEXT 株式會社以自家品牌final 與《新·福音戰士劇場版: ||》合作打造「EVA2020×final真無線耳機、3D入耳式耳機」,無線耳機售價日幣19800、3D入耳式耳機售價日幣2800,預計6月開售。
  • 萬字概覽 Java 虛擬機
    為什麼要學習 JVM在很多 Java 程式設計師的開發生涯裡,JVM 一直是黑盒子一般的存在,大家只知道運行 Java 程序需要依靠 JVM,千篇一律的配置幾個類似 -Xms 和 -Xmx 的參數,可能到最後都不知道自己配置的參數有什麼具體的意義。
  • Java 9正式發布,新特性解讀
    大家知道,Java 已經發展超過 20 年(95 年最初發布),Java 和相關生態在不斷豐富的同時也越來越暴露出一些問題,比如 Java 運行環境的膨脹和臃腫,各種類庫和工具在提供強大功能的同時,也越來越複雜,不同版本的類庫交叉依賴導致 Jar Hell 等讓人頭疼的問題,這些都阻礙了 Java 開發和運行效率的提升。
  • Java 訪問權限控制:你真的了解 protected 關鍵字嗎?
    特別地,如果你不希望其他任何人對該類擁有訪問權,你可以把所有的構造器都指定為private的,從而阻止任何人創建該類的對象。由於 protected 關鍵字的可見性內涵不太容易理解,我們在下一節專門進行介紹。三. protected 關鍵字的真正內涵  很多介紹Java語言的書籍(包括《Java編程思想》)都對protected介紹的比較的簡單,基本都是一句話,就是:被protected修飾的成員對於本包和其子類可見。這種說法有點太過含糊,常常會對大家造成誤解。
  • 「java」使用jwt進行認證授權
    先來看下jwt的java實現。<dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version
  • 「原創」為什麼java中一切都是對象,還要有static?
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫目錄1、static的「由來」2、static的使用場景3、關於static的常見問題4、總結Java是一種面向對象編程的語言,而對象是客觀存在的事物,對同類對象抽象出其共性,便是Java中的類,類是對象的模子,具有相同屬性和方法的一組對象的集合。
  • 「原創」讓設計模式飛一會兒|⑥面試必問代理模式
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文中所有的代碼和運行結果都是在amazon corretto openjdk 1.8環境中的,如果你不是使用該環境,可能會略有偏差。
  • Java基礎面試題簡單總結
    知道其行為的其它類可以在類中實現這些方法接口(interface)是抽象類的變體。在接口中,所有方法都是抽象的。多繼承性可通過實現這樣的接口而獲得。接口中的所有方法都是抽象的,沒有一個有程序體。接口只可以定義static final成員變量。接口的實現與子類相似,除了該實現類不能從接口定義中繼承行為。