并發(fā)編程之Java內(nèi)存模型volatile的內(nèi)存語義
1、volatile的特性
理解volatile
特性的一個(gè)好辦法是把對(duì)volatile
變量的單個(gè)讀/寫,看成是使用同一個(gè)鎖對(duì)單個(gè)讀/寫操作做了同步。
代碼示例:
package com.lizba.p1; /** * <p> * volatile示例 * </p> * * @Author: Liziba * @Date: 2021/6/9 21:34 */ public class VolatileFeatureExample { /** 使用volatile聲明64位的long型變量 */ volatile long v1 = 0l; /** * 單個(gè)volatile寫操作 * @param l */ public void set(long l) { v1 = l; } /** * 復(fù)合(多個(gè))volatile讀&寫 */ public void getAndIncrement() { v1++; } /** * 單個(gè)volatile變量的讀 * @return */ public long get() { return v1; } }
假設(shè)有多個(gè)線程分別調(diào)用上面程序的3個(gè)方法,這個(gè)程序在語義上和下面程序等價(jià)。
package com.lizba.p1; /** * <p> * synchronized等價(jià)示例 * </p> * * @Author: Liziba * @Date: 2021/6/9 21:46 */ public class SynFeatureExample { /** 定義一個(gè)64位長度的普通變量 */ long v1 = 0L; /** * 使用同步鎖對(duì)v1變量進(jìn)行寫操作 * @param l */ public synchronized void set(long l) { v1 = l; } /** * 通過同步讀和同步寫方法對(duì)v1進(jìn)行+1操作 */ public void getAndIncrement() { long temp = get(); // v1加一 temp += 1L; set(temp); } /** * 使用同步鎖對(duì)v1進(jìn)行讀操作 * @return */ public synchronized long get() { return v1; } }
如上兩個(gè)程序所示,一個(gè)volatile
變量的單個(gè)讀\寫操作,與一個(gè)普通變量的讀\寫操作都是使用同一個(gè)鎖來同步,它們之間的執(zhí)行效果相同。
上述代碼總結(jié):
鎖的happens-before
規(guī)則保證釋放鎖和獲取鎖的兩個(gè)線程之間的內(nèi)存可見性,這意味著對(duì)一個(gè)volatile
變量的讀,總能看到(任意線程)對(duì)這個(gè)volatile
變量最后的寫入。
鎖的語義決定了臨界區(qū)代碼的執(zhí)行具有原子性。這意味著,即使是64位的long型和double
型變量,只要它是volatile變量,對(duì)該變量的讀/寫就具有原子性。如果是多個(gè)volatile
操作或類似于volatile++
這種復(fù)合操作,這些操作整體上不具備原子性。
總結(jié)volatile特性:
- 可見性。對(duì)一個(gè)
volatile
變量的讀,總是能看到(任意線程)對(duì)這個(gè)volatile
變量最后的寫入。 - 原子性。對(duì)任意
volatile
變量的讀/寫具有原子性,但類似volatile++
這種復(fù)合操作不具有原子性。
2、volatile寫-讀建立的happens-before關(guān)系
- 對(duì)于程序員來說,我們更加需要關(guān)注的是volatile對(duì)線程內(nèi)存的可見性。
從JDK1.5(JSR-133)開始,volatile
變量的寫-讀可以實(shí)現(xiàn)線程之間的通信。從內(nèi)存語義的角度來說,volatile
的寫-讀與鎖的釋放-獲取有相同的內(nèi)存效果。
volatile
的寫和鎖的釋放有相同的內(nèi)存語義volatile
的讀和鎖的獲取有相同的內(nèi)存語義
代碼示例:
package com.lizba.p1; /** * <p> * * </p> * * @Author: Liziba * @Date: 2021/6/9 22:23 */ public class VolatileExample { int a = 0; volatile boolean flag = false; public void writer() { a = 1; // 1 flag = true; // 2 } public void reader() { if (flag) { // 3 int i = a; // 4 System.out.println(i); } } }
假設(shè)線程A執(zhí)行writer()方法之后,線程B執(zhí)行reader()
方法。根據(jù)happens-before
規(guī)則,
這個(gè)過程建立的happens-before關(guān)系如下:
- 根據(jù)程序次序規(guī)則,
1 happens-before 2
,3 happens-before 4
。 - 根據(jù)
volatile
規(guī)則,2 happens-before 3
。 - 根據(jù)
happens-before
的傳遞性規(guī)則,1 happens-before 4
。
圖示上述happens-before關(guān)系:
總結(jié):這里A線程寫一個(gè)volatile變量后,B線程讀同一個(gè)volatile變量。A線程在寫volatile變量之前所有可見的共享變量,在B線程讀同一個(gè)volatile變量后,將立即對(duì)B線程可見。
3、volatile寫-讀的內(nèi)存語義
volatile寫的內(nèi)存語義
當(dāng)寫一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存。
以上面的VolatileExample
為例,假設(shè)A線程首先執(zhí)行writer()
方法,隨后線程B執(zhí)行reader()
方法,初始時(shí)兩個(gè)線程的本地內(nèi)存中的flag和a都是初始狀態(tài)。
A執(zhí)行volatile寫后,共享變量狀態(tài)示意圖。
線程A在寫flag
變量后,本地內(nèi)存A中被線程A更新過的兩個(gè)共享變量的值被刷新到主內(nèi)存中,此時(shí)A的本地內(nèi)存和主內(nèi)存中的值是一致的。
volatile讀的內(nèi)存語義
當(dāng)讀一個(gè)volatile
變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無效。線程接下來將會(huì)從主內(nèi)存中讀取共享變量。
B執(zhí)行volatile讀后,共享變量的狀態(tài)示意圖:
在讀flag變量后,本地內(nèi)存B包含的值已經(jīng)被置為無效。此時(shí),線程B必須從主內(nèi)存中重新讀取共享變量。線程B的讀取操作將導(dǎo)致本地內(nèi)存B與主內(nèi)存中的共享變量的值變?yōu)橐恢隆?/p>
總結(jié)volatile的寫和volatile讀的內(nèi)存語義
- 線程A寫一個(gè)
volatile
變量,實(shí)質(zhì)上是線程A向接下來將要讀這個(gè)volatile
變量的某個(gè)線程發(fā)出了(其對(duì)共享變量所做修改的)消息。 - 線程B讀一個(gè)
volatile
變量,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的(在寫這個(gè)volatile
變量之前對(duì)共享變量所做修改的)消息。 - 線程A寫一個(gè)
volatile
變量,隨后線程B讀這個(gè)volatile
變量,這個(gè)過程實(shí)質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息。
4、volatile內(nèi)存語義實(shí)現(xiàn)
程序的重排序分為編譯器重排序和處理器重排序(我的前面的博文內(nèi)容有寫哈)。為了實(shí)現(xiàn)volatile
內(nèi)存語義,JMM會(huì)分別禁止這兩種類型的重排序。
volatile重排序規(guī)則表:
是否能重排序 | 第二個(gè)操作 | ||
---|---|---|---|
第一個(gè)操作 | 普通讀/寫 | volatile讀 | volatile寫 |
普通讀/寫 | NO | ||
volatile讀 | NO | NO | NO |
volatile寫 | NO | NO |
上圖舉例:第一行最后一個(gè)單元格意思是,在程序中第一個(gè)操作為普通讀/寫時(shí),如果第二個(gè)操作為volatile寫,則編譯器不能重排序。
總結(jié)上圖:
- 第二個(gè)操作是
volatile
寫時(shí),都不能重排序。確保volatile
寫之前的操作不會(huì)被編譯器重排序到volatile
之后 - 第一個(gè)操作為
volatile
讀時(shí),都不能重排序。確保volatile
讀之后的操作不會(huì)被編譯器重排序到volatile
之前 - 第一個(gè)操作為
volatile
寫,第二個(gè)操作為volatile
讀時(shí),不能重排序。
為了實(shí)現(xiàn)volatile
的內(nèi)存語義,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。
JMM采取的是保守策略內(nèi)存屏障插入策略,如下:
- 在每個(gè)
volatile
寫操作屏障前面插入一個(gè)StoreStore
屏障。 - 在每個(gè)
volatile
寫操作的后面插入一個(gè)StoreLoad
屏障 - 在每個(gè)
volatile
讀操作的后面插入一個(gè)LoadLoad
屏障。 - 在每個(gè)
volatile
讀操作的后面插入一個(gè)LoadStore
屏障。
保守策略可以保證在任意處理器平臺(tái)上,任意程序中都能得到正確的volatile
內(nèi)存語義。
保守策略下,volatile寫插入內(nèi)存屏障后生成的指令序列圖:
解釋:
StoreStore屏障可以保證在volatile寫之前,其前面所有普通寫操作已經(jīng)對(duì)任意處理器可見了。這是因?yàn)镾toreStore屏障將保障上面所有普通寫在volatile寫之前刷新到主內(nèi)存。
保守策略下,volatile讀插入內(nèi)存屏障后生成的指令序列圖:
解釋:
LoadLoad屏障用來禁止處理器把上面的volatile讀與下面的普通讀重排序。LoadStore屏障用來禁止處理器把上面的volatile讀與下面的普通寫重排序。
上述volatile
寫和volatile
讀的內(nèi)存屏障插入策略非常保守。在實(shí)際執(zhí)行時(shí),只要不改變volatile
寫-讀的內(nèi)存語義,編譯器可以根據(jù)具體情況省略不必要的屏障。
代碼示例:
package com.lizba.p1; /** * <p> * volatile屏障示例 * </p> * * @Author: Liziba * @Date: 2021/6/9 23:48 */ public class VolatileBarrierExample { int a; volatile int v1 = 1; volatile int v2 = 2; void readAndWrite() { // 第一個(gè)volatile讀 int i = v1; // 第二個(gè)volatile讀 int j = v2; // 普通寫 a = i + j; // 第一個(gè)volatile寫 v1 = i + 1; // 第二個(gè)volatile寫 v2 = j * 2; } // ... 其他方法 }
針對(duì)VolatileBarrierExample的readAndWrite(),編譯器生成字節(jié)碼時(shí)可以做如下優(yōu)化:
注意:最后的StoreLoad屏障無法省略。因?yàn)榈诙€(gè)volatile寫之后,程序return。此時(shí)編譯器無法準(zhǔn)確斷定后面是否會(huì)有volatile讀寫操作,為了安全起見,編譯器通常會(huì)在這里插入一個(gè)StoreLoad屏障。
上面的優(yōu)化可以針對(duì)任意處理器平臺(tái),但是由于不同的處理器有不同的“松緊度”的處理器內(nèi)存模型,內(nèi)存屏障的插入還可以根據(jù)具體的處理器內(nèi)存模型繼續(xù)優(yōu)化。
X86處理器平臺(tái)優(yōu)化
X86處理器僅會(huì)對(duì)寫-讀操作做重排序。X86不會(huì)對(duì)讀-讀、讀-寫和寫-寫重排序,因此X86處理器會(huì)省略掉這3種操作類型對(duì)應(yīng)的內(nèi)存屏障。在X86平臺(tái)中,JMM僅需要在volatile寫后插入一個(gè)StoreLoad屏障即可正確實(shí)現(xiàn)volatile寫-讀內(nèi)存語義。同時(shí)這樣意味著X86處理器中,volatile寫的開銷會(huì)遠(yuǎn)遠(yuǎn)大于讀的開銷。
5、volatile和鎖的比較
功能上:
鎖比
volatile
更強(qiáng)大
可伸縮性和執(zhí)行性能上:
volatile
更具有優(yōu)勢(shì)
到此這篇關(guān)于并發(fā)編程之Java內(nèi)存模型volatile的內(nèi)存語義的文章就介紹到這了,更多相關(guān)Java內(nèi)存模型volatile的內(nèi)存語義內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java web將數(shù)據(jù)導(dǎo)出為Excel格式文件代碼片段
這篇文章主要為大家詳細(xì)介紹了java web將數(shù)據(jù)導(dǎo)出為Excel格式文件代碼片段,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01springboot2.0如何通過fastdfs實(shí)現(xiàn)文件分布式上傳
這篇文章主要介紹了springboot2.0如何通過fastdfs實(shí)現(xiàn)文件分布式上傳,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12Java中List使用stream流轉(zhuǎn)成map的幾種方式詳解
Stream是Java8中處理集合的關(guān)鍵抽象概念,它可以指定你希望對(duì)集合進(jìn)行的操作,可以執(zhí)行非常復(fù)雜的查找、過濾和映射數(shù)據(jù)等操作,下面這篇文章主要給大家介紹了關(guān)于Java中List使用stream流轉(zhuǎn)成map的幾種方式,需要的朋友可以參考下2023-04-04Java中的static和final關(guān)鍵字的使用詳解
這篇文章主要介紹了Java中的static和final關(guān)鍵字的使用詳解, 當(dāng)方法名前有static,即為static方法,可以方便我們無需創(chuàng)建對(duì)象也可以調(diào)用此方法,靜態(tài)方法比較拉,只可以訪問 靜態(tài)的 屬性/變量/方法,無法訪問非靜態(tài)的這些屬性/變量/方法,需要的朋友可以參考下2024-01-01