日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
通過實例程序驗證與優(yōu)化談?wù)劸W(wǎng)上很多對于Java DCL的一些誤解

首先有這樣一個程序, 我們想實現(xiàn)一個單例值,只有第一次調(diào)用的時候初始化,并且有多線程會訪問這個單例值,那么我們會有:

getValue 的實現(xiàn)就是經(jīng)典的 DCL 寫法。

在 Java 內(nèi)存模型的限制下,這個 ValueHolder 有兩個潛在的問題:

  • 如果根據(jù) Java 內(nèi)存模型的定義,不考慮實際 JVM 的實現(xiàn),那么 getValue 是有可能返回 null 的。
  • 可能讀取到?jīng)]有初始化完成的 Value 的字段值。

下面我們就這兩個問題進行進一步分析并優(yōu)化。

根據(jù) Java 內(nèi)存模型的定義,不考慮實際 JVM 的實現(xiàn),getValue 有可能返回 null 的原因

在 全網(wǎng)最硬核 Java 新內(nèi)存模型解析與實驗 文章的7.1. Coherence(相干性,連貫性)與 Opaque中我們提到過:假設(shè)某個對象字段 int x 初始為 0,一個線程執(zhí)行:

另一個線程執(zhí)行(r1, r2 為本地變量):

那么這個實際上是兩次對于字段的讀取(對應(yīng)字節(jié)碼 getfield),在 Java 內(nèi)存模型下,可能的結(jié)果是包括:

  • r1 = 1, r2 = 1
  • r1 = 0, r2 = 1
  • r1 = 1, r2 = 0
  • r1 = 0, r2 = 0

其中第三個結(jié)果很有意思,從程序上理解即我們先看到了 x = 1,之后又看到了 x 變成了 0.實際上這是因為編譯器亂序。如果我們不想看到這個第三種結(jié)果,我們所需要的特性即 coherence。這里由于private Value value是普通的字段,所以根據(jù) Java 內(nèi)存模型來看并不保證 coherence。

回到我們的程序,我們有三次對字段讀取(對應(yīng)字節(jié)碼 getfield),分別位于:

由于 1,2 之間有明顯的分支關(guān)系(2 根據(jù) 1 的結(jié)果而執(zhí)行或者不執(zhí)行),所以無論在什么編譯器看來,都要先執(zhí)行 1 然后執(zhí)行 2。但是對于 1 和 3,他們之間并沒有這種依賴關(guān)系,在一些簡單的編譯器看來,他們是可以亂序執(zhí)行的。在 Java 內(nèi)存模型下,也沒有限制 1 與 3 之間是否必須不能亂序。所以,可能你的程序先執(zhí)行 3 的讀取,然后執(zhí)行 1 的讀取以及其他邏輯,最后方法返回 3 讀取的結(jié)果。

但是,在 OpenJDK Hotspot 的相關(guān)編譯器環(huán)境下,這個是被避免了的。OpenJDK Hotspot 編譯器是比較嚴謹?shù)木幾g器,它產(chǎn)生的 1 和 3 的兩次讀取(針對同一個字段的兩次讀取)也是兩次互相依賴的讀取,在編譯器維度是不會有亂序的(注意這里說的是編譯器維度哈,不是說這里會有內(nèi)存屏障連可能的 CPU 亂序也避免了,不過這里針對同一個字段讀取,前面已經(jīng)說了僅和編譯器亂序有關(guān),和 CPU 亂序無關(guān))

不過,這個僅僅是針對一般程序的寫法,我們可以通過一些奇怪的寫法騙過編譯器,讓他任務(wù)兩次讀取沒有關(guān)系,例如在全網(wǎng)最硬核 Java 新內(nèi)存模型解析與實驗 文章的7.1. Coherence(相干性,連貫性)與 Opaque中的實驗環(huán)節(jié),OpenJDK Hotspot 對于下面的程序是沒有編譯器亂序的:

但是如果你換成下面這種寫法,就騙過了編譯器:

我們不用太深究其原理,直接看其中一個結(jié)果:

對于 DCL 這種寫法,我們也是可以騙過編譯器的,但是一般我們不會這么寫,這里就不贅述了。

可能讀取到?jīng)]有初始化完成的 Value 的字段值。

這個就不只是編譯器亂序了,還涉及了 CPU 指令亂序以及 CPU 緩存亂序,需要內(nèi)存屏障解決可見性問題。

我們從 Value 類的構(gòu)造器入手:

對于 value = new Value(10); 這一步,將代碼分解為更詳細易于理解的偽代碼則是:

這中間沒有任何內(nèi)存屏障,根據(jù)語義分析,1 與 5 之間有依賴關(guān)系,因為 5 依賴于 1 的結(jié)果,必須先執(zhí)行 1 再執(zhí)行 5。 2 與 3 之間也是有依賴關(guān)系的,因為 3 依賴 2 的結(jié)果。但是,2和3,與 4,以及 5 這三個之間沒有依賴關(guān)系,是可以亂序的。我們使用使用代碼測試下這個亂序:

  • 雖然在注釋中寫出了這么編寫代碼的原因,但是這里還是想強調(diào)下這么寫的原因:
  • jcstress 的 @Actor 是使用一個線程執(zhí)行這個方法中的代碼,在測試中,每次會用不同的 JVM 啟動參數(shù)讓這段代碼解釋執(zhí)行,C1編譯執(zhí)行,C2編譯執(zhí)行,同時對于 JIT 編譯還會修改編譯參數(shù)讓它的編譯代碼效果不一樣。這樣我們就可以看到在不同的執(zhí)行方式下是否會有不同的編譯器亂序效果。
  • jcstress 的 @Actor 是使用一個線程執(zhí)行這個方法中的代碼,在每次使用不同的 JVM 測試啟動時,會將這個 @Actor 綁定到一個 CPU 執(zhí)行,這樣保證在測試的過程中,這個方法只會在這個 CPU 上執(zhí)行, CPU 緩存由這個方法的代碼獨占,這樣才能更容易的測試出 CPU 緩存不一致導(dǎo)致的亂序。所以,我們的 @Actor 注解方法的數(shù)量需要小于 CPU 個數(shù)。
  • 我們測試機這里只有兩個 CPU,那么只能有兩個線程,如果都執(zhí)行原始代碼的話,那么很可能都執(zhí)行到 synchronized 同步塊等待,synchronized 本身有內(nèi)存屏障的作用(后面會提到)。為了更容易測試出沒有走 synchronized 同步塊的情況,我們第二個@Actor 注解的方法直接去掉同步塊邏輯,并且如果 value 為 null,我們就設(shè)置結(jié)果都是 -1 用來區(qū)分。

我分別在 x86 和 arm CPU 上測試了這個程序,結(jié)果分別是:

x86 - AMD64:

arm - aarch64:

我們可以看到,在比較強一致性的 CPU 如 x86 中,是沒有看到未初始化的字段值的,但是在 arm 這種弱一致性的 CPU 上面,我們就看到了未初始化的值。在我的另一個系列 - 全網(wǎng)最硬核 Java 新內(nèi)存模型解析與實驗中,我們也多次提到了這個 CPU 亂序表格:

在這里,我們需要的內(nèi)存屏障是 StoreStore(同時我們也從上面的表格看出,x86 天生不需要 StoreStore,只要沒有編譯器亂序的話,CPU 層面是不會亂序的,而 arm 需要內(nèi)存屏障保證 Store 與 Store 不會亂序),只要這個內(nèi)存屏障保證我們前面?zhèn)未a中第 2,3 步在第 5 步前,第 4 步在第 5 步之前即可,那么我們可以怎么做呢?參考我的那篇全網(wǎng)最硬核 Java 新內(nèi)存模型解析與實驗中各種內(nèi)存屏障對應(yīng)關(guān)系,我們可以有如下做法,每種做法我們都會對比其內(nèi)存屏障消耗:

1.使用 final

final 是在賦值語句末尾添加 StoreStore 內(nèi)存屏障,所以我們只需要在第 2,3 步以及第 4 步末尾添加 StoreStore 內(nèi)存屏障即把 a2 和 b 設(shè)置成 final 即可,如下所示:

對應(yīng)偽代碼:

我們測試下:

這次在 arm 上的結(jié)果是:

如你所見,這次 arm CPU 上也沒有看到未初始化的值了。

這里 a1 不需要設(shè)置成 final,因為前面我們說過,2 與 3 之間是有依賴的,可以把他們看成一個整體,只需要整體后面添加好內(nèi)存屏障即可。但是這個并不可靠!!!!因為在某些 JDK 中可能會把這個代碼:

優(yōu)化成這樣:

這樣 a1, a2 之間就沒有依賴了!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!所以最好還是所有的變量都設(shè)置為 final

但是,這在我們不能將字段設(shè)置為 final 的時候,就不好使了。

2. 使用 volatile,這是大家常用以及官方推薦的做法

將 value 設(shè)置為 volatile 的,在我的另一系列文章 全網(wǎng)最硬核 Java 新內(nèi)存模型解析與實驗中,我們知道對于 volatile 寫入,我們通過在寫入之前加入 LoadStore + StoreStore 內(nèi)存屏障,在寫入之后加入 StoreLoad 內(nèi)存屏障實現(xiàn)的,如果把 value 設(shè)置為 volatile 的,那么前面的偽代碼就變成了:

我們通過下面的代碼測試下:

依舊在 arm 機器上面測試,結(jié)果是:

沒有看到未初始化值了。

3. 對于 Java 9+ 可以使用 Varhandle 的 acquire/release

前面分析,我們其實只需要保證在偽代碼第五步之前保證有 StoreStore 內(nèi)存屏障即可,所以 volatile 其實有點重,我們可以通過使用 Varhandle 的 acquire/release 這一級別的可見性 api 實現(xiàn),這樣偽代碼就變成了:

我們的測試代碼變成了:

測試結(jié)果是:

也是沒有看到未初始化值了。這種方式是用內(nèi)存屏障最少,同時不用限制目標類型里面不必使用 final 字段的方式。

4. 一種有趣但是沒啥用的思路 - 如果是靜態(tài)方法,可以通過類加載器機制實現(xiàn)很簡便的寫法

如果我們,ValueHolder 里面的方法以及字段可以是 static 的,例如:

將 ValueHolder 作為一個單獨的類,或者一個內(nèi)部類,這樣也是能保證 Value 里面字段的可見性的,這是通過類加載器機制實現(xiàn)的,在加載同一個類的時候(類加載的過程中會初始化 static 字段并且運行 static 塊代碼),是通過 synchronized 關(guān)鍵字同步塊保護的,參考其中類加載器(ClassLoader.java)的源碼:

ClassLoader.java

對于 syncrhonized 底層對應(yīng)的 monitorenter 和 monitorexit,monitorenter 與 volatile 讀有一樣的內(nèi)存屏障,即在操作之后加入 LoadLoad 和 LoadStore,monitorexit 與 volatile 寫有一樣的內(nèi)存屏障,在操作之前加入 LoadStore + StoreStore 內(nèi)存屏障,在操作之后加入 StoreLoad 內(nèi)存屏障。所以,也是能保證可見性的。但是這樣雖然寫起來貌似很簡便,效率上更加低(低了很多,類加載需要更多事情)并且不夠靈活,只是作為一種擴展知識知道就好。

總結(jié)

DCL 是一種常見的編程模式,對于鎖保護的字段 value 會有兩種字段可見性問題:

如果根據(jù) Java 內(nèi)存模型的定義,不考慮實際 JVM 的實現(xiàn),那么 getValue 是有可能返回 null 的。但是這個一般都被現(xiàn)在 JVM 設(shè)計避免了,這一點我們在實際編程的時候可以不考慮。

可能讀取到?jīng)]有初始化完成的 Value 的字段值,這個可以通過在構(gòu)造器完成與賦值給變量之間添加 StoreStore 內(nèi)存屏障解決??梢酝ㄟ^將 Value 的字段設(shè)置為 final 解決,但是不夠靈活。

最簡單的方式是將 value 字段設(shè)置為 volatile 的,這也是 JDK 中使用的方式,官方也推薦這種。

效率最高的方式是使用 VarHandle 的 release 模式,這個模式只會引入 StoreStore 與 LoadStore 內(nèi)存屏障,相對于 volatile 寫的內(nèi)存屏障要少很多(少了 StoreLoad,對于 x86 相當于沒有內(nèi)存屏障,因為 x86 天然有 LoadLoad,LoadStore,StoreStore,x86 僅僅不能天然保證 StoreLoad)。


網(wǎng)站欄目:通過實例程序驗證與優(yōu)化談?wù)劸W(wǎng)上很多對于Java DCL的一些誤解
文章位置:http://www.5511xx.com/article/dpgedge.html