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

2020-12-23 酷扯兒

本文轉載自【微信公眾號: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並發編程系列01|開篇獲獎感言
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫為什麼要學並發編程我曾聽一個從事15年開發工作的技術人員說過這個不寫並發程序的原則行得通的背景是那個時候基本都是單核處理器,系統並發量很低,藉助資料庫和類似Tomcat這種中間件就可以解決並發問題。如今硬體的驅動和網際網路行業的飛速發展,64核的處理器已經是很常見了,大型互聯廠商的系統並發量輕鬆過百萬,傳統的中間件和資料庫肯定是不能幫我們遮風避雨了,我們只能通過並發編程來解決這些問題。
  • 「原創」Java並發編程系列02|並發編程三大核心問題
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫寫在前面編寫並發程序是比較困難的,因為並發程序極易出現Bug,這些Bug有都是比較詭異的,很多都是沒辦法追蹤,而且難以復現。
  • 「原創」Java並發編程系列18|讀寫鎖(下)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第 18 篇,文末有本系列文章匯總。上篇為【原創】Java並發編程系列17 | 讀寫鎖八講(上),沒看過的可以先看看。本文是下篇,從「源碼分析寫鎖的獲取與釋放」開始。7.
  • 「原創」Java並發編程系列07|synchronized原理
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫並發編程中用到最多的關鍵字毫無疑問是synchronized。這篇文章就來探究下synchronized:synchronized如何使用?
  • 「原創」Java並發編程系列09|基礎乾貨
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第9篇。現在,我們進入正題:介紹並發編程的基礎性概念。
  • 「原創」Java並發編程系列14|AQS源碼分析
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第 14 篇,文末有本系列文章匯總。AbstractQueuedSynchronizer是Java並發包java.util.concurrent的核心基礎組件,是實現Lock的基礎。
  • 「原創」Java並發編程系列33|深入理解線程池(上)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫並發編程必不可少的線程池,接下來分兩篇文章介紹線程池,本文是第一篇。介紹1.1 使用場景並發編程可以高效利用CPU資源,提升任務執行效率,但是多線程及線程間的切換也伴隨著資源的消耗。當遇到單個任務處理時間比較短,但需要處理的任務數量很大時,線程會頻繁的創建銷毀,大量的時間和資源都會浪費在線程的創建和銷毀上,效率很低。
  • 「原創」Java並發編程系列26|ConcurrentHashMap(上)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫終於輪到ConcurrentHashMap了,並發編程必備,也是面試必備。HashMap 作為使用最頻繁的集合之一,在多線程環境下是不能用的,因為 HashMap 的設計上就沒有考慮並發環境,極易導致線程安全問題。為了解決該問題,提供了 Hashtable 和 Collections.synchronizedMap(hashMap)兩種解決方案,但是這兩種方案都是對讀寫加獨佔鎖,一個線程在讀時其他線程必須等待,吞吐量和性能都較低。
  • JAVA並發編程:並發問題的根源及主要解決方法
    同時,由於計算機領域的不斷發展,編譯器也越來越智能,它會自動對程式設計師編寫的代碼進行優化,而優化中就有可能出現實際執行代碼順序和編寫的代碼順序不一樣的情況。而這種破壞程序有序性的行為,在有些時候會出現一些非常微妙且難以察覺的並發編程bug。
  • Java並發編程系列20|StampedLock源碼解析
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第 20 篇,文末有本系列文章匯總。樂觀讀鎖:不需要維護鎖狀態,但是在具體操作數據前要檢查一下自己操作的數據是否經過修改操作,也就是驗證是否有線程獲取過寫鎖。你有沒有想過為什麼 state 要記錄寫鎖的獲取次數呢?
  • Java並發編程系列23|循環屏障CyclicBarrier
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本篇介紹第二個並發工具類CyclicBarrier,CyclicBarrier的字面意思是可循環使用(Cyclic)的屏障(Barrier),分以下部分介紹:CyclicBarrier的使用CyclicBarrier與CountDownLatch
  • Java並發編程系列21|Condition-Lock的等待通知
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫我們知道 synchronized 鎖通過 Object 類的 wait()和 notify()方法實現線程間的等待通知機制,而比 synchronized 更靈活 Lock 鎖同樣也有實現等待通知機制的方式
  • JAVA高並發網絡編程之BIO堵塞網絡編程
    上次說了網絡編程都是有作業系統統一的API的,每個語言有對它的實現,這次來一起說說通過java原生的socket編程完成BIO的網絡編程。
  • Java初學者系列:一文讀懂final關鍵字
    #程式設計師之前文章中我們學習了一些java中的關鍵字,通過這些關鍵字可以幫助我們定義類、實例、方法的可見性,今天我們一起來學習java中的final關鍵字,這個關鍵字同樣可以用來修飾類、實例、方法、參數等。
  • 適合Java新手的開源項目集合——在 GitHub 學編程
    經過之前小項目的小打小鬧,大家也有了一點編程的感覺了,是時候鞏固下理論知識了,俗話說的好:「基礎不牢,地動山搖」。現在這個階段去閱讀此著作一定會給你不一樣的感覺,我甚至推薦你每到一個階段,你可以回過頭去翻翻這本經典書籍,每次都會有新的認識。
  • Java 編程,哪些書值得推薦?
    書籍是人類進步的階梯,在網際網路不算發達的十年前(特指09年之前),學編程是要靠各種編程相關的書滴,大部分時間都必須沉迷於圖書館才能學好。現如今不同了,在線教育滿天飛的今天,如果你想學習編程打開App或者訪問相應的網站就行了。今非昔比。可為什麼還要讀書呢 ?答曰:讀書可以讓我們深化思想,很快地建立自己的知識脈絡,形成獨立的判斷。
  • Java並發編程:CountDownLatch、CyclicBarrier和Semaphore
    在java 1.5中,提供了一些非常有用的輔助類來幫助我們進行並發編程,比如CountDownLatch,CyclicBarrier和Semaphore,今天我們就來學習一下這三個輔助類的用法。InterruptedException { }; //和await()類似,只不過等待一定的時間後count值還沒變為0的話就會繼續執行 public void countDown() { }; //將count值減1下面看一個例子大家就清楚CountDownLatch的用法了:public class Test { public static void main(String[] args) { final
  • 給Java新手的一些建議——Java知識點歸納(Java基礎部分)
    Java的運行(基礎必備)這條可能出看很簡單,java程序的運行誰不會呢?不過很多時候, 我們只是單純通過IDE去執行java程序,底層IDE又是如何執行java程序呢?很多人並不了解。這個知識點是最最基本的java開發者需要掌握的,初學java,第一個肯定是教你如何在命令行中執行java程序,但是很多人一旦把java學完了,IDE用上了,就把這個都忘了。
  • [博客更新]Java 中 final 關鍵詞的使用
    上一篇文章我們講了 java 中 static 關鍵字的使用,這裡再將一下 final 關鍵字的使用。final 在 java 中可以看做一個「終結者」,它可以定義類、定義方法和定義變量。哎呀,好像沒別的可說了,就這樣吧,這可能是我寫過的字數最少的一篇文章了 算了,最後加一個示例程序吧,當然是錯誤的程序,包含了以上三種使用方法的錯誤使用報錯信息:package com.example.finalDemo;/** * User: sunriseydy * 2018-4-25 22:04 */finalclassPerson{String name
  • 大數據入門:Java和Scala編程對比
    在學習大數據之初,很多人都會對程式語言的學習有疑問,比如說大數據編程主要用什麼語言,在實際運用當中,大數據主流編程是Java,但是涉及到Spark、Kafka框架,還需要懂Scala。今天的大數據入門分享,我們就來對Java和Scala這兩門語言的編程做個對比。