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