Java內(nèi)存模型final的內(nèi)存語(yǔ)義
上篇并發(fā)編程之Java內(nèi)存模型volatile的內(nèi)存語(yǔ)義介紹了volatile
的內(nèi)存語(yǔ)義,本文講述的是final
的內(nèi)存語(yǔ)義,相比之下,final
域的讀和寫(xiě)更像是普通變量的訪問(wèn)。
1、final域的重排序規(guī)則final
對(duì)于final
域編譯器和處理器遵循兩個(gè)重排序規(guī)則
- 在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè)
final
域的寫(xiě)入,與隨后把這個(gè)對(duì)象的引用賦值給另一個(gè)引用變量,這兩個(gè)操作之間不能重排序 - 初次讀一個(gè)包含
final
域的對(duì)象的引用,與隨后初次讀這個(gè)final
域,這兩個(gè)操作之間不能重排序。
用代碼來(lái)說(shuō)明上面兩種重排序規(guī)則:
package com.lizba.p1; /** * <p> * * </p> * * @Author: Liziba * @Date: 2021/6/11 20:37 */ public class FinalExample { /** 普通變量 */ int i; /** final變量 */ final int j; /** 對(duì)象引用 */ static FinalExample obj; /** * 構(gòu)造函數(shù) */ public FinalExample() { // 寫(xiě)普通域 this.i = 1; // 寫(xiě)final域 this.j = 2; } /** * 線程A執(zhí)行writer寫(xiě)方法 * */ public static void writer() { obj = new FinalExample(); } /** * 線程B執(zhí)行reader讀方法 * */ public static void reader() { // 讀對(duì)象的引用 FinalExample finalExample = obj; // 讀普通域 int a = finalExample.i; // 讀final域 int b = finalExample.j; } }
假設(shè)線程A執(zhí)行writer()
方法,線程B執(zhí)行reader()
方法。下面來(lái)通過(guò)這兩個(gè)線程的交互來(lái)說(shuō)明這兩個(gè)規(guī)則。
2、寫(xiě)final域的重排序規(guī)則
寫(xiě)final域的重排序禁止吧final域的寫(xiě)重排序到構(gòu)造函數(shù)之外。通過(guò)如下方式來(lái)實(shí)現(xiàn):
- JMM禁止編譯器把
final
域的寫(xiě)重排序到構(gòu)造函數(shù)之外 - 編譯器會(huì)在final域的寫(xiě)之后,構(gòu)造函數(shù)return之前,插入一個(gè)
StoreStore
屏障。這個(gè)屏障禁止處理器把final
域的寫(xiě)重排序到構(gòu)造函數(shù)之外。
現(xiàn)在開(kāi)始分析writer()方法:
/** * 線程A執(zhí)行writer寫(xiě)方法 * */ public static void writer() { obj = new FinalExample(); }
- 構(gòu)造一個(gè)
FinalExample
類型的對(duì)象 - 將對(duì)象的引用賦值給變量
obj
首先假設(shè)線程B讀對(duì)象引用與讀對(duì)象的成員域之間沒(méi)有重排序,則下圖是其一種執(zhí)行可能
線程執(zhí)行時(shí)序圖:
3、讀final與的重排序規(guī)則
讀final域的重排序規(guī)則是,在一個(gè)線程中,初次讀對(duì)象引用與初次讀該對(duì)象包含的final
域,JMM
禁止處理器重排序這兩個(gè)操作(注意是處理器)。編譯器會(huì)在讀final
域操作的前面插入一個(gè)LoadLoad
屏障。
解釋:初次讀對(duì)象引用與初次讀該對(duì)象包含的final域,這兩個(gè)操作之間存在間接依賴關(guān)系。
- 編譯器遵守間接依賴關(guān)系,編譯器不會(huì)重排序這兩個(gè)操作
- 大多數(shù)處理器也遵守間接依賴,不會(huì)重排序這兩個(gè)操作。但是少部分處理器允許對(duì)存在間接依賴關(guān)系的操作做重排序(比如
alpha
處理器),這個(gè)規(guī)則就是專門(mén)針對(duì)這種處理器的。
分析reader()方法:
/** * 線程B執(zhí)行reader讀方法 * */ public static void reader() { // 讀對(duì)象的引用 FinalExample finalExample = obj; // 讀普通域 int a = finalExample.i; // 讀final域 int b = finalExample.j; }
- 初次讀引用變量obj
- 初次讀引用變量obj指向?qū)ο蟮钠胀ㄓ騤
- 初次讀引用變量obj指向?qū)ο蟮膄inal域i
假設(shè)B線程所處的處理器不遵守間接依賴關(guān)系,且A線程執(zhí)行過(guò)程中沒(méi)有發(fā)生任何重排序,此時(shí)存在如下的執(zhí)行時(shí)序:
線程執(zhí)行時(shí)序圖:
上圖B線程中讀對(duì)象的普通域被重排序到處理器讀取對(duì)象引用之前, 此時(shí)普通域i還沒(méi)有被線程A寫(xiě)入,因此這是一個(gè)錯(cuò)誤的讀取操作。但是final域的讀取會(huì)被重排序規(guī)則把讀final域的操作“限定”在讀該final域所屬對(duì)象的引用讀取之后,此時(shí)final域已經(jīng)被正確的初始化了,這是一個(gè)正確的讀取操作。
總結(jié):
讀final域的重排序規(guī)則可以確保,在讀一個(gè)對(duì)象的final域之前,一定會(huì)先讀包含這個(gè)final域的對(duì)象的引用。
4、final域?yàn)橐妙愋?/h2>
上面講述了基礎(chǔ)數(shù)據(jù)類型,如果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; /** * 構(gòu)造函數(shù) */ public FinalReferenceExample() { this.intArray = new int[1]; // 1 intArray[0] = 1; // 2 } /** * 寫(xiě)線程A執(zhí)行 */ public static void writer1() { obj = new FinalReferenceExample(); // 3 } /** * 寫(xiě)線程B執(zhí)行 */ public static void writer2() { obj.intArray[0] = 2; // 4 } /** * 讀線程C執(zhí)行 */ public static void reader() { if (obj != null) { // 5 int temp = obj.intArray[0]; // 6 } } }
如上final
域?yàn)橐粋€(gè)int類型的數(shù)組的引用變量。對(duì)應(yīng)引用類型,寫(xiě)final
域的重排序?qū)幾g器和處理器增加了如下約束:
- 在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè)
final
引用的對(duì)象的成員域的寫(xiě)入,與隨后在構(gòu)造函數(shù)外把這個(gè)被構(gòu)造對(duì)象的引用賦值給另一個(gè)引用變量,這兩個(gè)操作不能重排序。
對(duì)于上述程序,假設(shè)A執(zhí)行writer1()方法,執(zhí)行完后線程B執(zhí)行writer2()方法,執(zhí)行完后線程C執(zhí)行reader()方法。則存在如下線
程執(zhí)行時(shí)序:引用型final的執(zhí)行時(shí)序圖
JMM對(duì)于上述代碼,可以確保讀線程C至少能看到寫(xiě)線程A在構(gòu)造函數(shù)中對(duì)final引用對(duì)象的成員域的寫(xiě)入。即寫(xiě)線程C至少能看到數(shù)組下標(biāo)0的值為1。但是寫(xiě)線程B對(duì)數(shù)組元素的寫(xiě)入,讀線程C可能看得到可能看不到。JMM不能保證線程B的寫(xiě)入對(duì)讀線程C可見(jiàn)。因?yàn)閷?xiě)線程B和讀線程C之間存在數(shù)據(jù)競(jìng)爭(zhēng),此時(shí)的執(zhí)行結(jié)果不可預(yù)知。
此時(shí)如果想確保讀線程C看到寫(xiě)線程B對(duì)數(shù)組元素的寫(xiě)入,可以結(jié)合同步原語(yǔ)(volatile
或者lock
)來(lái)實(shí)現(xiàn)。
5、為什么final引用不能從構(gòu)造函數(shù)內(nèi)“逸出”
本文一直在說(shuō)寫(xiě)final
域的重排序規(guī)則可以確保:在引用變量為任意線程可見(jiàn)之前,該引用變量指向的對(duì)象的final域已經(jīng)在構(gòu)造函數(shù)中被正確初始化了。那究竟是如何實(shí)現(xiàn)的呢?
其實(shí)這需要另一個(gè)條件:在構(gòu)造函數(shù)內(nèi)部,不能讓這個(gè)被構(gòu)造對(duì)象的引用被其它線程所見(jiàn)。也就是對(duì)象引用不能在構(gòu)造函數(shù)中“逸出”。
示例代碼:
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、寫(xiě)final域 obj = this; // 2、this引用在此處"逸出" } public static void writer() { new FinalReferenceEscapeExample(); } public static void reader() { if (obj != null) { // 3 int temp = obj.i; // 4 } } }
假設(shè)線程A執(zhí)行writer()
方法,線程B執(zhí)行reader()
方法。這里操作2導(dǎo)致對(duì)象還未完成構(gòu)造前就對(duì)線程B可見(jiàn)了。因?yàn)?和2允許重排序,所以線程B可能無(wú)法看到final域被正確初始化后的值。實(shí)際執(zhí)行的時(shí)序圖可能如下所示:
多線程執(zhí)行時(shí)序圖:
總結(jié):
在構(gòu)造函數(shù)返回之前,被構(gòu)造對(duì)象的引用不能為其他線程可見(jiàn),因?yàn)榇藭r(shí)的final域可能還沒(méi)被初始化。而在構(gòu)造函數(shù)返回后,任意線程都將保證能看到final域正確初始化之后的值。
6、final語(yǔ)義在處理器中的實(shí)現(xiàn)
舉例X86處理器中final語(yǔ)義的具體實(shí)現(xiàn)。
在編譯器中會(huì)存在如下的處理:
- 寫(xiě)final域的重排序規(guī)則會(huì)要求編譯器在
final
域的寫(xiě)之后,構(gòu)造函數(shù)return
之前插入一個(gè)StoreStore
屏障 - 讀final域的重排序規(guī)則要求編譯器在讀
final
域的操作前插入一個(gè)LoadLoad
屏障
但是,由于X86處理器不會(huì)對(duì)寫(xiě)-寫(xiě)操作做重排序,所以在X86處理器中,寫(xiě)final域需要的StoreStore
屏障會(huì)被省略。同樣,由于X86處理器不會(huì)對(duì)存在間接依賴關(guān)系的操作做重排序,所以在X86處理器中,讀final域需要的LoadLoad
屏障也會(huì)被省略掉。因此,在X86處理器中,final域的讀/寫(xiě)不會(huì)插入任何內(nèi)存屏障。
7、JSR-133為什么要增強(qiáng)final的語(yǔ)義
在舊的Java內(nèi)存模型中,一個(gè)最嚴(yán)重的缺陷就是現(xiàn)場(chǎng)可能看到final
域的值會(huì)改變。比如一個(gè)線程讀取一個(gè)被final域的值為0(未初始化之前的默認(rèn)值),過(guò)一段時(shí)間再讀取初始化后的final
域的值,卻發(fā)現(xiàn)變?yōu)榱?。因此為了修復(fù)此漏洞,JSR-133增強(qiáng)了final語(yǔ)義。
總結(jié):
通過(guò)為final增加寫(xiě)和讀重排序規(guī)則,可以為Java程序員提供初始化安全保障:只要對(duì)象正確構(gòu)造(被構(gòu)造對(duì)象額引用在構(gòu)造函數(shù)中沒(méi)有“逸出”),那么不需要使用同步原語(yǔ)(volatile和lock的使用)就可以保障任意線程都能看到這個(gè)final域在構(gòu)造函數(shù)中被初始化之后的值。
到此這篇關(guān)于Java內(nèi)存模型final的內(nèi)存語(yǔ)義的文章就介紹到這了,更多相關(guān)Java內(nèi)存模型final的內(nèi)存語(yǔ)義內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javaweb設(shè)計(jì)中filter粗粒度權(quán)限控制代碼示例
這篇文章主要介紹了javaweb設(shè)計(jì)中filter粗粒度權(quán)限控制代碼示例,小編覺(jué)得還是挺不錯(cuò)的,需要的朋友可以參考。2017-10-10Java構(gòu)建JDBC應(yīng)用程序的實(shí)例操作
在本篇文章里小編給大家整理了一篇關(guān)于Java構(gòu)建JDBC應(yīng)用程序的實(shí)例操作,有興趣的朋友們可以學(xué)習(xí)參考下。2021-03-03在idea中將java項(xiàng)目中的單個(gè)類打包成jar包操作
這篇文章主要介紹了在idea中將java項(xiàng)目中的單個(gè)類打包成jar包操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08一文教會(huì)你如何從0到1搭建一個(gè)SpringBoot項(xiàng)目
今天剛好學(xué)習(xí)到SpringBoot,就順便記錄一下吧,下面這篇文章主要給大家介紹了關(guān)于如何從0到1搭建一個(gè)SpringBoot項(xiàng)目的相關(guān)資料,文中通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01SpringBoot @ExceptionHandler與@ControllerAdvice異常處理詳解
在Spring Boot應(yīng)用的開(kāi)發(fā)中,不管是對(duì)底層數(shù)據(jù)庫(kù)操作,對(duì)業(yè)務(wù)層操作,還是對(duì)控制層操作,都會(huì)不可避免的遇到各種可預(yù)知的,不可預(yù)知的異常需要處理,如果每個(gè)處理過(guò)程都單獨(dú)處理異常,那么系統(tǒng)的代碼耦合度會(huì)很高,工作量大且不好統(tǒng)一,以后維護(hù)的工作量也很大2022-10-10