最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

Volatile:JVM 我警告你,我的人你別亂動!

2022-08-08 22:02 作者:劉水鏡  | 我要投稿

Volatile 算是一個面試中的高頻問題了。我們都知道 Volatile 有兩個作用:

  1. 禁止指令重排

  2. 保證內(nèi)存可見

指令重排序

指令重排序的問題,基本上都是通過 DCL 問題來考察。

DCL,Double Check Look

面試中通常會是下面這種情景:

面試官:用過單例嗎?

你:用過。

面試官:如何實現(xiàn)一個線程安全的懶漢式單例

你:DCL

面試官:DCL 可以保證線程絕對安全嗎?

你:加 Volatile。

面試官滿意的點點頭。通常情況下,面試中這個問題聊到這里也就結(jié)束了。

但這個問題,還有一些可挖掘的內(nèi)容。我們順著單例的代碼繼續(xù)往下挖:

如果不加 Volatile,會有什么問題呢?問題就出現(xiàn)在下面這行代碼:

上面這行代碼看起來也平平無奇呀,就是一個賦值操作,還能整什么幺蛾子呢?我們只寫了一行代碼,但 JVM 則需要做好幾步操作。那 JVM 究竟干了啥呢?大概也許可能差不多就是把大象給放冰箱里了(如果這句看不懂,請咨詢宋丹丹老師)。

Java 代碼中的一條賦值語句,到了 JVM 指令層面大概分三步:

  1. 分配一塊內(nèi)存空間

  2. 初始化

  3. 返回內(nèi)存地址

下面通過字節(jié)碼來一探究竟,為了簡化問題,我們替換成下面的代碼:

編譯以后,通過 javap -v 命令,或者 IDEA 中的 JClassLib 插件可以看到如下圖所示的內(nèi)容:

關(guān)于 Java 字節(jié)碼,可以戳這里:《寫了那么多 Java 代碼,卻不一定見過它的真面目》

通過上面的字節(jié)碼信息,可以更加清楚的看到上面提到的那三個步驟

  1. new 用來分配一塊內(nèi)存空間

  2. invokspecial 調(diào)用了 Object 的 init() 方法,做了初始化

  3. astore_1 就是將 o 指向了 Object 實例對象的內(nèi)存地址,完成賦值

dup 指令會做一些入棧操作,跟我們要討論的問題關(guān)系不大,這里可以先忽略?!禞ava 程序在 JVM 中是怎樣執(zhí)行的?》中有一個視頻動畫更形象的說明了這一點。

到這里,問題就比較明了了。重排的問題會發(fā)生在第 2 和 3 步。因為先初始化還是先把對象的內(nèi)存地址賦值給 o,并沒有必然的前后制約關(guān)系。因此,這類的指令在某些情況下會被重排序。

單線程下,這種重排序完全沒有問題。但是多線程的場景下,就有可能出問題:A 線程進(jìn)入到 instance = new Singleton(); 后,由于指令重排,在 init 之前,將地址給了 o。此時 B 線程來了,發(fā)現(xiàn) instance 不為 null,于是直接拿去用了,然而此時 instance 并沒有初始化,只是個半成品。所以,當(dāng) B 拿到 instance 進(jìn)行操作的時候就會出現(xiàn)問題了。

因此,instance 需要使用 volatile 來修飾,從而禁止進(jìn)行指令重排。

到這里,你可能要說了,我用單例不加 volatile,這么長時間了也沒遇到你說的重排序問題。你怎么證明「重排序」的存在呢?好問題,下面咱們通過一個小例子來驗證一下重排序是否真的存在。

代碼很簡單,就是幾個賦值操作,但卻很巧妙。x、y、a、b 初始都為 0,兩個線程分別給 a、x 和 b、y 賦值,線程 one 先讓 a = 1,然后再讓 x = b;two 線程先讓 b = 1,然后再讓 y = a。

假如不發(fā)生重排序,那么以上程序只會有下面六種可能:

每一列,從上到下代表代碼執(zhí)行的順序。

也就是說,在沒有重排序的情況下,不可能出現(xiàn) x、y 同時為 0 的情況。而如果 x、y 同時為 0 了,那么一定是出現(xiàn)了下面六種情況中的一種,既發(fā)生了重排。

每一列,從上到下代表代碼執(zhí)行的順序。

運行程序,經(jīng)過漫長的等待,得到了如下的輸出:

可以看到,在執(zhí)行了五十多萬次以后,我們終于捕捉到了一次重排序。發(fā)生這種情況的幾率很低,所以你就算沒有用 volatile 大概率不會有問題,但我們在今后還是要合理的使用 volatile。

內(nèi)存可見性

聊完指令重排,接下來聊聊內(nèi)存可見。這次我們直接上代碼:

代碼很簡單,主線程內(nèi)開啟一個子線程,子線程中一個 while 循環(huán),當(dāng) flag 為 false 時,結(jié)束循環(huán)。flag 初始值為 true,一秒鐘后,被主線程設(shè)置為 false。

按照上面這個邏輯,子線程應(yīng)該會在程序啟動一秒后停止。然而,當(dāng)你運行程序后會發(fā)現(xiàn),這個程序就像吃了炫邁一樣,根本停不下來。

這說明主線程對 flag 的修改,子線程并沒有感知到。我們修改一下程序:

為 flag 加上 volatile 修飾符,再次運行,你會發(fā)現(xiàn)程序運行后,很快(大概一秒鐘)就停止了。這是為啥?是炫邁的藥勁兒過了嗎?

哈哈,當(dāng)然不是。為了更好的性能,線程都有自己的緩存(CPU 中的高速緩存),我們稱之為工作內(nèi)存或者本地內(nèi)存。還有一塊公共內(nèi)存,我們叫它主從吧。它們的結(jié)構(gòu)大致如下圖所示:

主存中定義了一個 flag 變量,每個線程讀取它的時候,為了更好的性能會在線程本地緩存一份它的副本。讀取的時候也是優(yōu)先讀取本地副本的值。當(dāng) flag 被 volatile 修飾后,每次被修改,都會讓其他線程中的副本失效,從而必須去主存中讀取最新的值。所以,在使用了 volatile 后,子線程能夠立即感知到 flag 的變化,從而停止。

上圖簡化了線程(CPU)的緩存結(jié)構(gòu),其完整結(jié)構(gòu)如下圖所示:

現(xiàn)代 CPU 共有三級緩存,分別為:L1、L2 和 L3。CPU 中的每個核心都有自己的 L1 和 L2,而一顆 CPU 中的多個核心會共享 L3。

總結(jié)

Volatile 的意思是,易變的,動蕩不定的,反復(fù)無常的。volatile 的作用就是告訴 JVM,被我修飾的變量它非常善變,你要給我盯好了,一旦有風(fēng)吹草動要立馬通知大家;另外,你不要自作聰明的調(diào)整它的位置(為了性能重排序),它可是說翻臉就翻臉的主兒。

最后,留一個小問題:內(nèi)存可見性的那個程序中,就算 flag 沒有被 volatile 修飾,線程頂多不是第一時間讀到 flag 的修改,但也不應(yīng)該一直讀不到呀,這是為啥?這太反直覺了!

開動你的腦筋思考一下吧!


價值 107 元的《Spring Boot趣味實戰(zhàn)課》免費送啦!https://bbs.csdn.net/topics/607586357


Volatile:JVM 我警告你,我的人你別亂動!的評論 (共 條)

分享到微博請遵守國家法律
嘉峪关市| 金秀| 潜江市| 墨竹工卡县| 丹阳市| 武鸣县| 巴林左旗| 朝阳市| 黄骅市| 离岛区| 丘北县| 阳春市| 重庆市| 若尔盖县| 仁化县| 独山县| 舞阳县| 德庆县| 襄汾县| 剑河县| 遵义市| 昆山市| 平利县| 青州市| 城步| 开封县| 长乐市| 华亭县| 肥东县| 五家渠市| 南乐县| 诸暨市| 通海县| 白山市| 泸州市| 邯郸市| 濉溪县| 兴业县| 温州市| 新乡市| 霍城县|