從內(nèi)存模型中了解Java final的全部細(xì)節(jié)
茫茫人海千千萬(wàn)萬(wàn),感謝這一秒你看到這里。希望我的文章對(duì)你的有所幫助!
愿你在未來(lái)的日子,保持熱愛(ài),奔赴山海??!
從這篇文章開(kāi)始,帶你深入了解final
的細(xì)節(jié)!
?? 從內(nèi)存模型中了解final
在上面,我們了解在單線程情況下的final
,但對(duì)于多線程并發(fā)下的final
,你有了解嗎?多線程并發(fā)的話,我們又必須知道一個(gè)內(nèi)存模型的概念:JMM
。
?? JMM
JMM是定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存在主內(nèi)存(MainMemory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(LocalMemory)即共享變量副本,本地內(nèi)存中存儲(chǔ)了該線程以讀、寫(xiě)共享變量的副本。本地內(nèi)存是Java內(nèi)存模型的一個(gè)抽象概念,并不真實(shí)存在。它涵蓋了緩存、寫(xiě)緩沖區(qū)、寄存器等。
而在這一內(nèi)存模型下,計(jì)算機(jī)在執(zhí)行程序時(shí),為了提高性能,編譯器和處理器常常會(huì)對(duì)指令做重排序。那么問(wèn)題又來(lái)了,重排序是什么?
?? 重排序
其實(shí)對(duì)于我們程序來(lái)說(shuō),可以分為不同指令,每一個(gè)指令都會(huì)包含多個(gè)步驟,每個(gè)步驟可能使用不同的硬件。我們可以將每個(gè)指令拆分為五個(gè)階段:
想這樣如果是按順序串行執(zhí)行指令,那可能相對(duì)比較慢,因?yàn)樾枰却弦粭l指令完成后,才能等待下一步執(zhí)行:
而如果發(fā)生指令重排序呢,實(shí)際上雖然不能縮短單條指令的執(zhí)行時(shí)間,但是它變相地提高了指令的吞吐量,可以在一個(gè)時(shí)鐘周期內(nèi)同時(shí)運(yùn)行五條指令的不同階段。
我們來(lái)分析下代碼的執(zhí)行情況,并思考下:
a = b + c;
d = e - f ;
按原先的思路,會(huì)先加載b和c,再進(jìn)行b+c操作賦值給a,接下來(lái)就會(huì)加載e和f,最后就是進(jìn)行e-f操作賦值給d。
這里有什么優(yōu)化的空間呢?我們?cè)趫?zhí)行b+c操作賦值給a時(shí),可能需要等待b和c加載結(jié)束,才能再進(jìn)行一個(gè)求和操作,所以這里可能出現(xiàn)了一個(gè)停頓等待時(shí)間,依次后面的代碼也可能會(huì)出現(xiàn)停頓等待時(shí)間,這降低了計(jì)算機(jī)的執(zhí)行效率。
為了去減少這個(gè)停頓等待時(shí)間,我們可以先加載e和f,然后再去b+c操作賦值給a,這樣做對(duì)程序(串行)是沒(méi)有影響的,但卻減少了停頓等待時(shí)間。既然b+c操作賦值給a需要停頓等待時(shí)間,那還不如去做一些有意義的事情。
總結(jié):指令重排對(duì)于提高CPU處理性能十分必要。但是會(huì)因此引發(fā)一些指令的亂序。那么我們的final
它對(duì)指令重排序有什么作用呢?接下來(lái)我們來(lái)看看吧!
?? final域重排序規(guī)則
對(duì)于JMM內(nèi)存模型來(lái)說(shuō),它對(duì)final
域有以下兩種重排序規(guī)則:
- 寫(xiě):在構(gòu)造函數(shù)內(nèi)對(duì)
final
域?qū)懭耄S后將構(gòu)造函數(shù)的引用賦值給一個(gè)引用變量,操作不能重排序。 - 讀:初次讀一個(gè)包含
final
域的對(duì)象的引用和隨后初次寫(xiě)這個(gè)final
域,不能重排序。
具體我們根據(jù)代碼演示一邊來(lái)講解吧:
代碼:
package com.ygt.test; /** * 測(cè)試JMM內(nèi)存模型對(duì)final域重排序的規(guī)則 */ public class JMMFinalTest { // 普通變量 private int variable; // final變量 private final int variable2; private static JMMFinalTest jmmFinalTest; // 構(gòu)造方法中,將普通變量和final變量進(jìn)行寫(xiě)的操作 public JMMFinalTest(){ variable = 1; // 1. 寫(xiě)普通變量 variable2 = 2; // 2. 寫(xiě)final變量 } // 模仿一個(gè)寫(xiě)操作 --> 假設(shè)線程A進(jìn)行來(lái)寫(xiě)操作 public static void write() { // new 當(dāng)前類對(duì)象 --> 并在構(gòu)造函數(shù)中完成賦值操作 jmmFinalTest = new JMMFinalTest(); } // 模仿一個(gè)讀操作 --> 假設(shè)線程B進(jìn)行來(lái)讀操作 public static void read() { // 讀操作: JMMFinalTest test = jmmFinalTest; // 3. 讀對(duì)象的引用 int localVariable = test.variable; int localVariable2 = test.variable2; } }
寫(xiě)final域重排序規(guī)則
寫(xiě)final
域重排序規(guī)則在構(gòu)造函數(shù)內(nèi)對(duì)final
域?qū)懭?,隨后將構(gòu)造函數(shù)的引用賦值給一個(gè)引用變量,操作不能重排序。代表禁止對(duì)final
域的初始化操作必須在構(gòu)造函數(shù)中,不能重排序到構(gòu)造函數(shù)之外,這個(gè)規(guī)則的實(shí)現(xiàn)主要包含了兩個(gè)方面:
- JMM內(nèi)存模型禁止編譯器把
final
域的寫(xiě)重排序到構(gòu)造函數(shù)之外; - 編譯器會(huì)在
final
域?qū)懭牒蜆?gòu)造函數(shù)return返回之前,插入一個(gè)storestore
內(nèi)存屏障。這個(gè)內(nèi)存屏障可以禁止處理器把final
域的寫(xiě)重排序到構(gòu)造函數(shù)之外。
我們?cè)賮?lái)分析write方法,雖然只有一行代碼,但他實(shí)際上有三個(gè)步驟:
- 在JVM的堆中申請(qǐng)一塊內(nèi)存空間
- 對(duì)象進(jìn)行初始化操作
- 將堆中的內(nèi)存空間的引用地址賦值給一個(gè)引用變量jmmFinalTest。
對(duì)于普通變量variable來(lái)說(shuō),它的初始化操作可以被重排序到構(gòu)造函數(shù)之外,即我們的步驟不是本來(lái)1-2-3嗎,現(xiàn)在可能造成1-3-2這樣初始化操作在構(gòu)造函數(shù)返回后了!
而對(duì)于final
變量variable2來(lái)說(shuō),它的初始化操作一定在構(gòu)造函數(shù)之內(nèi),即1-2-3。
我們來(lái)看一個(gè)可能發(fā)生的圖:
對(duì)于變量的可見(jiàn)性來(lái)說(shuō),因?yàn)槠胀ㄗ兞縱ariable可能會(huì)發(fā)生重排序的一個(gè)現(xiàn)象,讀取的值可能會(huì)不一樣,可能是0或者是1。但是final
變量variable2,它讀取的值一定是2了,因?yàn)橛袀€(gè)StoreStore
內(nèi)存屏障來(lái)保證與下面的操作進(jìn)行重排序的操作。
由此可見(jiàn),寫(xiě)final
域的重排序規(guī)則可以哪怕保證我們?cè)趯?duì)象引用為任意線程可見(jiàn)之前,對(duì)象的final域已經(jīng)被正確初始化過(guò)了,而普通域就不具有這個(gè)保障。
讀final域重排序規(guī)則
初次讀一個(gè)包含final
域的對(duì)象的引用和隨后初次寫(xiě)這個(gè)final
域,不能重排序。怎么實(shí)現(xiàn)呢?
它其實(shí)處理器會(huì)在讀final
域操作的前面插入一個(gè)LoadLoad
內(nèi)存屏障。
我們?cè)賮?lái)分析read方法,他實(shí)有三個(gè)步驟:
- 初次讀引用變量jmmFinalTest;
- 初次讀引用變量jmmFinalTest的普通域變量variable;
- 初次讀引用變量jmmFinalTest的
final
域變量variable2;
我們以寫(xiě)操作正常排序的情況,對(duì)于讀情況可能發(fā)生圖解:
對(duì)于讀對(duì)象的普通域變量variable可能發(fā)生重排序的現(xiàn)象,被重排序到了讀對(duì)象引用的前面,此時(shí)就會(huì)出現(xiàn)線程B還未讀到對(duì)象引用就在讀取該對(duì)象的普通域變量,這顯然是錯(cuò)誤的操作。
而對(duì)于final
域的讀操作通過(guò)LoadLoad
內(nèi)存屏障保證在讀final
域變量前已經(jīng)讀到了該對(duì)象的引用,從而就可以避免以上情況的發(fā)生。
由此可見(jiàn),讀final
域的重排序規(guī)則可以確保我們?cè)谧x一個(gè)對(duì)象的final域之前,一定會(huì)先讀這個(gè)包含這個(gè)final域的對(duì)象的引用,而普通域就不具有這個(gè)保障。
?? final對(duì)象是引用類型
上面我已經(jīng)了解了final
域?qū)ο笫腔緮?shù)據(jù)類型的一個(gè)重排序規(guī)則了,但是對(duì)象如果是引用類型呢?我們接著來(lái):
當(dāng)final
域?qū)ο笫且粋€(gè)引用類型,寫(xiě)final
域的重排序規(guī)則增加了如下的約束:
在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè)final
引用的對(duì)象的成員域的寫(xiě)入,與隨后在構(gòu)造函數(shù)外將被構(gòu)造對(duì)象的引用賦值給引用變量之間不能重排序。 聽(tīng)起來(lái)還是有點(diǎn)難懂是吧,沒(méi)事,代碼看看!
注意一點(diǎn):之前的寫(xiě)final
域的重排序規(guī)則一樣存在,只是對(duì)引用類型對(duì)象增加了一條規(guī)則。
代碼:
package com.ygt.test; /** * 測(cè)試final引用類型對(duì)象時(shí)的讀寫(xiě)情況 */ public class ReferenceFinalTest { // 定義引用對(duì)象 final Person person; private ReferenceFinalTest referenceFinalTest; // 在構(gòu)造函數(shù)中初始化,并進(jìn)行賦值操作 public ReferenceFinalTest(){ person = new Person(); // 1. 初始化 person.setName("詹姆斯!"); // 2. 賦值 } // 線程A進(jìn)來(lái)進(jìn)行寫(xiě)操作,實(shí)現(xiàn)將referenceFinalTest初始化 public void write(){ referenceFinalTest = new ReferenceFinalTest(); // 3. 初始化構(gòu)造函數(shù) } // 線程B進(jìn)來(lái)進(jìn)行寫(xiě)操作,實(shí)現(xiàn)person重新賦值操作。 public void write2(){ person.setName("戴維斯"); // 4. 重新賦值操作 } // 線程C進(jìn)來(lái)進(jìn)行讀操作,讀取當(dāng)前person的值 public void read(){ if(referenceFinalTest != null) { // 5. 讀取引用對(duì)象 String name = person.getName(); // 6. 讀取person對(duì)象的值 } } } class Person{ private String name; private int age; public void setName(String name) { this.name = name; } public String getName() { return name; } }
首先,我們先畫(huà)個(gè)可能發(fā)生情況的圖解:
我們線程的執(zhí)行順序:A ——> B ——> C
接著我們對(duì)讀寫(xiě)操作方法進(jìn)行詳解:
寫(xiě)final域重排序規(guī)則
從之前我們就知道,我們final
域的寫(xiě)禁止重排序到構(gòu)造方法外,因此1和3是不能發(fā)生重排序現(xiàn)象滴。
而對(duì)于我們新增的約束來(lái)說(shuō),在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè)final
引用的對(duì)象的成員域的寫(xiě)入,與隨后在構(gòu)造函數(shù)外將被構(gòu)造對(duì)象的引用賦值給引用變量之間不能重排序。即final
域的引用對(duì)象的成員屬性寫(xiě)入setName("詹姆斯")
是不可以與隨后將這個(gè)被構(gòu)造出來(lái)的對(duì)象賦給引用變量jmmFinalTest重排序,因此2和3不能重排序。
所以我們的步驟是1-2-3。
讀final域重排序規(guī)則
對(duì)于多線程情況下,JMM內(nèi)存模型至少可以確保線程C在讀對(duì)象person的成員屬性時(shí),先讀取到了引用對(duì)象person了,可以讀取到線程A對(duì)final
域引用對(duì)象person的成員屬性的寫(xiě)入。
可能此時(shí)線程B對(duì)于person的成員屬性的寫(xiě)入暫時(shí)看不到,保證不了線程B的寫(xiě)入對(duì)線程C的可見(jiàn)性,因?yàn)榭赡芫€程B與線程C存在了線程搶占的競(jìng)爭(zhēng)問(wèn)題,此時(shí)的結(jié)果可能不同!
當(dāng)然,如果想要保存可見(jiàn),我們可以使用Volatile或者同步鎖。
?? 小結(jié)
我們可以根據(jù)數(shù)據(jù)類型分類:
基本數(shù)據(jù)類型:
- 寫(xiě):在構(gòu)造函數(shù)內(nèi)對(duì)
final
域?qū)懭?,隨后將構(gòu)造函數(shù)的引用賦值給一個(gè)引用變量,操作不能重排序。即禁止final
域?qū)懼嘏判虻綐?gòu)造方法之外。 - 讀:初次讀一個(gè)包含
final
域的對(duì)象的引用和隨后初次寫(xiě)這個(gè)final
域,不能重排序。
引用數(shù)據(jù)類型:
在基本數(shù)據(jù)類型上額外增加約束:
禁止在構(gòu)造函數(shù)對(duì)一個(gè)final修飾的對(duì)象的成員域?qū)傩缘膶?xiě)入與隨后將這個(gè)被構(gòu)造的對(duì)象的引用賦值給引用變量進(jìn)行重排序。
??總結(jié)
相信各位看官都對(duì)final這一個(gè)關(guān)鍵字有了一定了解吧,其實(shí)額外擴(kuò)展自己的知識(shí)面也是相當(dāng)有必要滴,不然別人追問(wèn)你的時(shí)候,你會(huì)啞口無(wú)言,而一旦你自己每天都深入剖析知識(shí)點(diǎn)后,你在今后的對(duì)答中都會(huì)滔滔不絕,綻放光芒的?。。?duì)吧,我們還有一把東西等著我們探索和摸索中!接下來(lái)就是潛心學(xué)習(xí)一段時(shí)間,不浮躁,不氣餒!
讓我們也一起加油吧!本人不才,如有什么缺漏、錯(cuò)誤的地方,也歡迎各位人才大佬評(píng)論中批評(píng)指正!當(dāng)然如果這篇文章確定對(duì)你有點(diǎn)小小幫助的話,也請(qǐng)親切可愛(ài)的人才大佬們給個(gè)點(diǎn)贊、收藏下吧,一鍵三連,非常感謝!
學(xué)到這里,今天的世界打烊了,晚安!雖然這篇文章完結(jié)了,但是我還在,永不完結(jié)。我會(huì)努力保持寫(xiě)文章。來(lái)日方長(zhǎng),何懼車遙馬慢!
感謝各位看到這里!愿你韶華不負(fù),青春無(wú)悔!
到此這篇關(guān)于從內(nèi)存模型中了解Java final的全部細(xì)節(jié)的文章就介紹到這了,更多相關(guān)Java final內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于OpenCv與JVM實(shí)現(xiàn)加載保存圖像功能(JAVA?圖像處理)
openCv有一個(gè)名imread的簡(jiǎn)單函數(shù),用于從文件中讀取圖像,本文給大家介紹JAVA?圖像處理基于OpenCv與JVM實(shí)現(xiàn)加載保存圖像功能,感興趣的朋友一起看看吧2022-01-01詳解mybatis foreach collection示例
這篇文章主要介紹了詳解mybatis foreach collection的相關(guān)資料,需要的朋友可以參考下2017-10-10SpringBoot HttpMessageConverter消息轉(zhuǎn)換器的使用詳解
在整個(gè)數(shù)據(jù)流轉(zhuǎn)過(guò)程中,前端的請(qǐng)求報(bào)文轉(zhuǎn)化為Java對(duì)象,Java對(duì)象轉(zhuǎn)化為響應(yīng)報(bào)文,這里就用到了消息轉(zhuǎn)換器HttpMessageConverter2022-06-06java通過(guò)信號(hào)量實(shí)現(xiàn)限流的示例
本文主要介紹了java通過(guò)信號(hào)量實(shí)現(xiàn)限流的示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06詳解DES加密算法的原理與Java實(shí)現(xiàn)
DES 加密,是對(duì)稱加密。對(duì)稱加密,顧名思義,加密和解密的運(yùn)算全都是使用的同樣的秘鑰。這篇文章主要為大家講講DES加密算法的原理與Java實(shí)現(xiàn),需要的可以參考一下2022-10-10