一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務器之家 - 編程語言 - Java教程 - Java內存模型final的內存語義

Java內存模型final的內存語義

2022-03-11 00:43李子捌 Java教程

這篇文章主要介紹了Java內存模型final的內存語義,上篇介紹volatile的內存語義,本文講述的是final的內存語義,相比之下,final域的讀和寫更像是普通變量的訪問。下面我們一起來看看文章學校內容吧,需要的朋友可以參考一下

上篇并發編程之Java內存模型volatile的內存語義介紹了volatile的內存語義,本文講述的是final的內存語義,相比之下,final域的讀和寫更像是普通變量的訪問。

1、final域的重排序規則final

對于final域編譯器和處理器遵循兩個重排序規則

  • 在構造函數內對一個final域的寫入,與隨后把這個對象的引用賦值給另一個引用變量,這兩個操作之間不能重排序
  • 初次讀一個包含final域的對象的引用,與隨后初次讀這個final域,這兩個操作之間不能重排序。

用代碼來說明上面兩種重排序規則:

package com.lizba.p1;

/**
* <p>
*
* </p>
*
* @Author: Liziba
* @Date: 2021/6/11 20:37
*/
public class FinalExample {

  /** 普通變量 */
  int i;
  /** final變量 */
  final int j;
  /** 對象引用 */
  static FinalExample obj;

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

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

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

假設線程A執行writer()方法,線程B執行reader()方法。下面來通過這兩個線程的交互來說明這兩個規則。

 

2、寫final域的重排序規則

寫final域的重排序禁止吧final域的寫重排序到構造函數之外。通過如下方式來實現:

  • JMM禁止編譯器把final域的寫重排序到構造函數之外
  • 編譯器會在final域的寫之后,構造函數return之前,插入一個StoreStore屏障。這個屏障禁止處理器把final域的寫重排序到構造函數之外。

現在開始分析writer()方法:

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

  • 構造一個FinalExample類型的對象
  • 將對象的引用賦值給變量obj

首先假設線程B讀對象引用與讀對象的成員域之間沒有重排序,則下圖是其一種執行可能

線程執行時序圖:

Java內存模型final的內存語義

 

3、讀final與的重排序規則

讀final域的重排序規則是,在一個線程中,初次讀對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操作(注意是處理器)。編譯器會在讀final域操作的前面插入一個LoadLoad屏障。

解釋:初次讀對象引用與初次讀該對象包含的final域,這兩個操作之間存在間接依賴關系。

  • 編譯器遵守間接依賴關系,編譯器不會重排序這兩個操作
  • 大多數處理器也遵守間接依賴,不會重排序這兩個操作。但是少部分處理器允許對存在間接依賴關系的操作做重排序(比如alpha處理器),這個規則就是專門針對這種處理器的。

分析reader()方法:

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

  • 初次讀引用變量obj
  • 初次讀引用變量obj指向對象的普通域j
  • 初次讀引用變量obj指向對象的final域i

假設B線程所處的處理器不遵守間接依賴關系,且A線程執行過程中沒有發生任何重排序,此時存在如下的執行時序:

線程執行時序圖:

Java內存模型final的內存語義

上圖B線程中讀對象的普通域被重排序到處理器讀取對象引用之前, 此時普通域i還沒有被線程A寫入,因此這是一個錯誤的讀取操作。但是final域的讀取會被重排序規則把讀final域的操作“限定”在讀該final域所屬對象的引用讀取之后,此時final域已經被正確的初始化了,這是一個正確的讀取操作。

總結:

讀final域的重排序規則可以確保,在讀一個對象的final域之前,一定會先讀包含這個final域的對象的引用。

 

4、final域為引用類型

上面講述了基礎數據類型,如果final域修飾的引用類型又該如何?

package com.lizba.p1;

/**
* <p>
*      final 修飾引用類型變量
* </p>
*
* @Author: Liziba
* @Date: 2021/6/11 21:52
*/
public class FinalReferenceExample {

  /** final是引用類型 */
  final int[] intArray;
  static FinalReferenceExample obj;
  
  /**
   * 構造函數
   */
  public FinalReferenceExample() {
      this.intArray = new int[1];  // 1
      intArray[0] = 1;             // 2
  }

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

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

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

如上final域為一個int類型的數組的引用變量。對應引用類型,寫final域的重排序對編譯器和處理器增加了如下約束:

  • 在構造函數內對一個final引用的對象的成員域的寫入,與隨后在構造函數外把這個被構造對象的引用賦值給另一個引用變量,這兩個操作不能重排序。

對于上述程序,假設A執行writer1()方法,執行完后線程B執行writer2()方法,執行完后線程C執行reader()方法。則存在如下線

程執行時序:引用型final的執行時序圖

Java內存模型final的內存語義

JMM對于上述代碼,可以確保讀線程C至少能看到寫線程A在構造函數中對final引用對象的成員域的寫入。即寫線程C至少能看到數組下標0的值為1。但是寫線程B對數組元素的寫入,讀線程C可能看得到可能看不到。JMM不能保證線程B的寫入對讀線程C可見。因為寫線程B和讀線程C之間存在數據競爭,此時的執行結果不可預知。

此時如果想確保讀線程C看到寫線程B對數組元素的寫入,可以結合同步原語(volatile或者lock)來實現。

 

5、為什么final引用不能從構造函數內“逸出”

本文一直在說寫final域的重排序規則可以確保:在引用變量為任意線程可見之前,該引用變量指向的對象的final域已經在構造函數中被正確初始化了。那究竟是如何實現的呢?

其實這需要另一個條件:在構造函數內部,不能讓這個被構造對象的引用被其它線程所見。也就是對象引用不能在構造函數中“逸出”。

示例代碼:

package com.lizba.p1;

/**
* <p>
*   final引用逸出demo
* </p>
*
* @Author: Liziba
* @Date: 2021/6/11 22:33
*/
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
      }
  }
}

假設線程A執行writer()方法,線程B執行reader()方法。這里操作2導致對象還未完成構造前就對線程B可見了。因為1和2允許重排序,所以線程B可能無法看到final域被正確初始化后的值。實際執行的時序圖可能如下所示:

多線程執行時序圖:

Java內存模型final的內存語義

總結:

在構造函數返回之前,被構造對象的引用不能為其他線程可見,因為此時的final域可能還沒被初始化。而在構造函數返回后,任意線程都將保證能看到final域正確初始化之后的值。

 

6、final語義在處理器中的實現

舉例X86處理器中final語義的具體實現。

在編譯器中會存在如下的處理:

  • 寫final域的重排序規則會要求編譯器在final域的寫之后,構造函數return之前插入一個StoreStore屏障
  • 讀final域的重排序規則要求編譯器在讀final域的操作前插入一個LoadLoad屏障

但是,由于X86處理器不會對寫-寫操作做重排序,所以在X86處理器中,寫final域需要的StoreStore屏障會被省略。同樣,由于X86處理器不會對存在間接依賴關系的操作做重排序,所以在X86處理器中,讀final域需要的LoadLoad屏障也會被省略掉。因此,在X86處理器中,final域的讀/寫不會插入任何內存屏障。

 

7、JSR-133為什么要增強final的語義

在舊的Java內存模型中,一個最嚴重的缺陷就是現場可能看到final域的值會改變。比如一個線程讀取一個被final域的值為0(未初始化之前的默認值),過一段時間再讀取初始化后的final域的值,卻發現變為了1。因此為了修復此漏洞,JSR-133增強了final語義。

總結:

通過為final增加寫和讀重排序規則,可以為Java程序員提供初始化安全保障:只要對象正確構造(被構造對象額引用在構造函數中沒有“逸出”),那么不需要使用同步原語(volatile和lock的使用)就可以保障任意線程都能看到這個final域在構造函數中被初始化之后的值。

到此這篇關于Java內存模型final的內存語義的文章就介紹到這了,更多相關Java內存模型final的內存語義內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!

原文鏈接:https://juejin.cn/post/7017979592119943198

延伸 · 閱讀

精彩推薦
  • Java教程較詳細的JNI簡介

    較詳細的JNI簡介

    JNI是本地語言編程接口。它允許運行在JVM中的Java代碼和用C、C++或匯編寫的本地代碼相互操作。下面通過本文給大家分享JNI簡介,感興趣的朋友一起看看吧...

    動力節點9362021-01-21
  • Java教程Java內存模型的深入講解

    Java內存模型的深入講解

    這篇文章主要給大家介紹了關于Java內存模型的相關資料,我們常說的JVM內存模式指的是JVM的內存分區,而Java內存模式是一種虛擬機規范,需要的朋友可以參考...

    surzia11132021-09-27
  • Java教程Java線程編程中的主線程講解

    Java線程編程中的主線程講解

    這篇文章主要介紹了Java線程編程中的主線程,是Java入門學習中的基礎知識,需要的朋友可以參考下 ...

    goldensun4762020-01-07
  • Java教程Java基于TCP方式的二進制文件傳輸

    Java基于TCP方式的二進制文件傳輸

    這篇文章主要為大家介紹了Java基于TCP方式的二進制文件傳輸,一個基于Java Socket協議之上文件傳輸的完整示例,基于TCP通信完成,感興趣的小伙伴們可以參...

    gloomyfish1592020-03-22
  • Java教程Java基于二維數組實現的數獨問題示例

    Java基于二維數組實現的數獨問題示例

    這篇文章主要介紹了Java基于二維數組實現的數獨問題,涉及java針對數組的遍歷、計算、轉換等相關操作技巧,需要的朋友可以參考下...

    handsome_ZHANG9602021-03-16
  • Java教程Mybatis工具類JdbcTypeInterceptor運行時自動添加jdbcType屬性

    Mybatis工具類JdbcTypeInterceptor運行時自動添加jdbcType屬性

    今天小編就為大家分享一篇關于Mybatis工具類JdbcTypeInterceptor運行時自動添加jdbcType屬性,小編覺得內容挺不錯的,現在分享給大家,具有很好的參考價值,需...

    isea5334422021-06-25
  • Java教程Spring Cloud中Eureka開啟密碼認證的實例

    Spring Cloud中Eureka開啟密碼認證的實例

    這篇文章主要介紹了Spring Cloud中Eureka開啟密碼認證的實例,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧...

    無始之名7462021-05-04
  • Java教程深層剖析java應用開發中MyBayis緩存

    深層剖析java應用開發中MyBayis緩存

    這篇文章主要為大家深層剖析java開發中MyBayis緩存,文中講解了Mybatis緩存的分類以及使用的方式,有需要的朋友可以借鑒參考下,希望可以有所幫助...

    DrLai8272022-01-04
主站蜘蛛池模板: 动漫精品一区二区三区3d | 脱女学小内内摸出水网站免费 | 果冻传媒在线播放1 | 公交车强校花系列小说 | 性xxxxbbbbxxxx中国 | 男生同性视频twink在线 | 人人人人人看碰人人免费 | 香蕉免费看一区二区三区 | 国产大片51精品免费观看 | 免费岛国片 | 久久九九亚洲精品 | 日韩二区三区 | 欧美日本一区视频免费 | 99热精品国产麻豆 | 美国69xxxx59 | 精品国产91久久久久久久 | 我与肥熟老妇的性事 | 韩国三级视频网站 | 好逼天天有 | 村上里沙40分钟在线观看 | 亚洲欧美日韩成人 | 青青久在线视频免费观看 | 四虎影视永久在线 | 果冻传媒天美传媒乌鸦传媒 | 国产精品亚洲片夜色在线 | 精品国产品国语在线不卡丶 | www四虎影视| 天天爱天天做天天爽天天躁 | 门房秦大爷小说 | 亚洲天堂伦理 | 国产美女操 | 九九九九九九伊人 | 精品精品国产自在现拍 | 亚洲风情无码免费视频 | 精品麻豆国产 | 亚洲另类第一页 | 无码国产成人午夜在线观看不卡 | 精品性影院一区二区三区内射 | 加勒比一本大道在线 | 亚洲精品久久久成人 | 青草国产在线观看 |