volatile 的底層實(shí)現(xiàn)原理是內(nèi)存屏障饿序。Memory Barrier
- 對(duì)volatile寫(xiě)指令后會(huì)加入寫(xiě)屏障
- 對(duì)volatile讀指令前會(huì)加入讀屏障
如何保證可見(jiàn)性
寫(xiě)屏障保證在該屏障之前葡秒,對(duì)共享變量的改動(dòng)谒养,都同步到主存中去撇贺。
int number = -1;
volatile boolean ready =false;
@Actor
public void actor2(I_Result r){
number =2;
ready=true; //ready是 volatile 有寫(xiě)屏障
//寫(xiě)屏障
}
讀屏障保證在該屏障之后,對(duì)共享變量的讀取育八,都是最新的數(shù)據(jù)。
@Actor
public void actor1(I_Result r){
//讀屏障
// ready是volatile斯够,帶讀屏障
if(ready){
r.r1 = number+number;
}else{
r.r1=1;
}
}
如何保證有序性
寫(xiě)屏障會(huì)確保指令重排時(shí),不會(huì)將寫(xiě)屏障之前的代碼排在寫(xiě)屏障之后
@Actor
public void actor2(I_Result r){
number =2;
ready=true; //ready是 volatile 有寫(xiě)屏障
//寫(xiě)屏障
}
讀屏障會(huì)確保指令重排是喧锦,不會(huì)將寫(xiě)屏障之后的代碼排在讀屏障之前
@Actor
public void actor1(I_Result r){
//讀屏障
// ready是volatile读规,帶讀屏障
if(ready){
r.r1 = number+number;
}else{
r.r1=1;
}
}
volatile不能解決指令交錯(cuò)的問(wèn)題。它只是解決了可見(jiàn)性燃少,和有序性的問(wèn)題束亏。而有序性也只是解決了線(xiàn)程內(nèi)部不進(jìn)行指令重排。
volatile如何保證可見(jiàn)性
package com.conrrentcy.atomic;
public class VisibilityVolatile {
public static void main(String[] args) throws InterruptedException {
Monitor1 monitor = new Monitor1();
monitor.startMonitor();
Thread.sleep(1000);
monitor.stop();
Monitor2 monitor2 = new Monitor2();
monitor2.startMonitor();
Thread.sleep(1000);
monitor2.stop();
}
}
class Monitor1 {
Thread monitor = null;
private volatile boolean isRunning = true;
public boolean isRunning() {
return isRunning;
}
public void stop() {
this.isRunning = false;
}
public void startMonitor() {
monitor = new Thread(() -> {
while (true) {
if (!isRunning) {
break;
}
}
});
monitor.start();
}
}
class Monitor2 {
Thread monitor = null;
private Object lock = new Object();
private boolean isRunning = true;
public boolean isRunning() {
return isRunning;
}
public void stop() {
synchronized (lock) {
this.isRunning = false;
}
}
public void startMonitor() {
monitor = new Thread(() -> {
while (true) {
synchronized (lock) {
if (!isRunning) {
break;
}
}
}
});
monitor.start();
}
}
利用工具h(yuǎn)sdis供汛,打印出匯編指令枪汪,可以發(fā)現(xiàn)涌穆,加了volatile修飾之后打印出來(lái)的匯編指令多了下面一行:
0x000000010f030785: lock addl $0x0,(%rsp) ;*putfield queue
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
hsdis-amd64.dylib放在$JAVA_PATH/jre/lib/server/中
lock是一種控制指令怔昨,在多處理器環(huán)境下,lock 匯編指令可以基于總線(xiàn)鎖或者緩存鎖的機(jī)制來(lái)達(dá)到可見(jiàn)性的一個(gè)效果宿稀。
- StoreStoreBarrier|volatile 寫(xiě)操作|StoreLoadBarrier(寫(xiě)寫(xiě)屏障和寫(xiě)讀屏障)
- LoadLoadBarrier|volatile 讀操作|LoadStoreBarrier(讀讀屏障和讀寫(xiě)屏障)
CPU層面的內(nèi)存屏障
CPU內(nèi)存屏障主要分為以下三類(lèi):
- 寫(xiě)屏障(Store Memory Barrier):告訴處理器在寫(xiě)屏障之前的所有已經(jīng)存儲(chǔ)在存儲(chǔ)緩存(store bufferes)中的數(shù)據(jù)同步到主內(nèi)存趁舀,簡(jiǎn)單來(lái)說(shuō)就是使得寫(xiě)屏障之前的指令的結(jié)果對(duì)寫(xiě)屏障之后的讀或者寫(xiě)是可見(jiàn)的。
- 讀屏障(Load Memory Barrier):處理器在讀屏障之后的讀操作,都在讀屏障之后執(zhí)行祝沸。配合寫(xiě)屏障矮烹,使得寫(xiě)屏障之前的內(nèi)存更新對(duì)于讀屏障之后的讀操作是可見(jiàn)的。
- 全屏障(Full Memory Barrier):確保屏障前的內(nèi)存讀寫(xiě)操作的結(jié)果提交到內(nèi)存之后罩锐,再執(zhí)行屏障后的讀寫(xiě)操作奉狈。
JVM層面
在JVM層面,定義了一種抽象的內(nèi)存模型(JMM)來(lái)規(guī)范并控制重排序涩惑,從而解決可見(jiàn)性問(wèn)題仁期。
JMM(Java內(nèi)存模型)
JMM全稱(chēng)是Java Memory Model(Java內(nèi)存模型),什么是JMM呢?通過(guò)前面的分析發(fā)現(xiàn)竭恬,導(dǎo)致可見(jiàn)性問(wèn)題的根本原因是緩存以及指令重排序跛蛋。 而JMM 實(shí)際上就是提供了合理的禁用緩存以及禁止重排序的方法。所以JMM最核心的價(jià)值在于解決可見(jiàn)性和有序性痊硕。
JMM屬于語(yǔ)言級(jí)別的抽象內(nèi)存模型赊级,可以簡(jiǎn)單理解為對(duì)硬件模型的抽象,它定義了共享內(nèi)存中多線(xiàn)程程序讀寫(xiě)操作的行為規(guī)范岔绸,通過(guò)這些規(guī)則來(lái)規(guī)范對(duì)內(nèi)存的讀寫(xiě)操作從而保證指令的正確性理逊,它解決了CPU 多級(jí)緩存橡伞、處理器優(yōu)化、指令重排序?qū)е碌膬?nèi)存訪(fǎng)問(wèn)問(wèn)題晋被,保證了并發(fā)場(chǎng)景下的可見(jiàn)性骑歹。
需要注意的是,JMM并沒(méi)有限制執(zhí)行引擎使用處理器的寄存器或者高速緩存來(lái)提升指令執(zhí)行速度墨微,也沒(méi)有限制編譯器對(duì)指令進(jìn)行重排序道媚,也就是說(shuō)在JMM中,也會(huì)存在緩存一致性問(wèn)題和指令重排序問(wèn)題翘县。只是JMM把底層的問(wèn)題抽象到JVM層面最域,再基于CPU層面提供的內(nèi)存屏障指令,以及限制編譯器的重排序來(lái)解決并發(fā)問(wèn)題锈麸。
JMM抽象模型結(jié)構(gòu)
JMM 抽象模型分為主內(nèi)存镀脂、工作內(nèi)存欺矫;主內(nèi)存是所有線(xiàn)程共享的舅锄,一般是實(shí)例對(duì)象、靜態(tài)字段舷丹、數(shù)組對(duì)象等存儲(chǔ)在堆內(nèi)存中的變量氓奈。工作內(nèi)存是每個(gè)線(xiàn)程獨(dú)占的翘魄,線(xiàn)程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,不能直接讀寫(xiě)主內(nèi)存中的變量舀奶,線(xiàn)程之間的共享變量值的傳遞都是基于主內(nèi)存來(lái)完成暑竟,可以抽象為下圖:
JMM如何解決可見(jiàn)性
從JMM的抽象模型結(jié)構(gòu)圖來(lái)看,如果線(xiàn)程A與線(xiàn)程B之間要通信的話(huà)育勺,必須要經(jīng)歷下面2個(gè)步驟但荤。
1)線(xiàn)程A把本地內(nèi)存A中更新過(guò)的共享變量刷新到主內(nèi)存中去。
2)線(xiàn)程B到主內(nèi)存中去讀取線(xiàn)程A之前已更新過(guò)的共享變量涧至。
下面通過(guò)示意圖來(lái)說(shuō)明這兩個(gè)步驟:
結(jié)合上圖腹躁,假設(shè)初始時(shí),這3個(gè)內(nèi)存中的x值都為0南蓬。線(xiàn)程A在執(zhí)行時(shí)纺非,把更新后的x值(假設(shè)值為1)臨時(shí)存放在自己的本地內(nèi)存 A中。當(dāng)線(xiàn)程A和線(xiàn)程B需要通信時(shí)蓖康,線(xiàn)程A首先會(huì)把自己本地內(nèi)存中修改后的x值刷新到主內(nèi) 存中铐炫,此時(shí)主內(nèi)存中的x值變?yōu)榱?。隨后蒜焊,線(xiàn)程B到主內(nèi)存中去讀取線(xiàn)程A更新后的x值倒信,此時(shí)線(xiàn)程B的本地內(nèi)存的x值也變?yōu)榱?。 從整體來(lái)看泳梆,這兩個(gè)步驟實(shí)質(zhì)上是線(xiàn)程A在向線(xiàn)程B發(fā)送消息鳖悠,而且這個(gè)通信過(guò)程必須要經(jīng)過(guò)主內(nèi)存榜掌。JMM通過(guò)控制主內(nèi)存與每個(gè)線(xiàn)程的本地內(nèi)存之間的交互,來(lái)為Java程序員提供內(nèi)存可見(jiàn)性保證乘综。
JMM層面的內(nèi)存屏障
在JMM 中把內(nèi)存屏障分為四類(lèi):
持該屏障(其他類(lèi)型的屏障不一定被所有處理器支持)憎账。執(zhí)行該屏障開(kāi)銷(xiāo)會(huì)很昂貴,因?yàn)楫?dāng)前處理器通常要把寫(xiě)緩沖區(qū)中的數(shù)據(jù)全部刷新到內(nèi)存中(Buffer Fully Flush)卡辰。
JMM Happen-Before 原則
happens-before的概念最初由Leslie Lamport在其一篇影響深遠(yuǎn)的論文(《Time胞皱,Clocks and the Ordering of Events in a Distributed System》)中提出。
《JSR-133:Java Memory Model and Thread Specification》對(duì)happens-before關(guān)系的定義如下:
(1)程序順序規(guī)則:一個(gè)線(xiàn)程中的每個(gè)操作九妈,happens-before于該線(xiàn)程中的任意后續(xù)操作反砌。
(2)監(jiān)視器鎖規(guī)則:對(duì)一個(gè)鎖的解鎖,happens-before于隨后對(duì)這個(gè)鎖的加鎖萌朱。
(3)volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫(xiě)宴树,happens-before于任意后續(xù)對(duì)這個(gè)volatile域的讀。
(4)傳遞性:如果A happens-before B晶疼,且B happens-before C酒贬,那么A happens-before C。
(5)start()規(guī)則:如果線(xiàn)程A執(zhí)行操作ThreadB.start()(啟動(dòng)線(xiàn)程B)翠霍,那么A線(xiàn)程的ThreadB.start()操作happens-before于線(xiàn)程B中的任意操作锭吨。
(6)Join()規(guī)則:如果線(xiàn)程A執(zhí)行操作ThreadB.join()并成功返回,那么線(xiàn)程B中的任意操作happens-before于線(xiàn)程A從ThreadB.join()操作成功返回壶运。
(7)程序中斷規(guī)則:對(duì)線(xiàn)程interrupted()方法的調(diào)用先行于被中斷線(xiàn)程的代碼檢測(cè)到中斷時(shí)間的發(fā)生耐齐。
(8)對(duì)象finalize規(guī)則:一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行于發(fā)生它的finalize()方法的開(kāi)始浪秘。
Lock 前綴指令有內(nèi)存屏障的作用蒋情。
一共有4種內(nèi)存屏障,分別是 LoadLoad耸携、LoadStore棵癣、StoreStore、StoreLoad夺衍。
- LoadLoad:確保 Load1 數(shù)據(jù)的讀取先于 Load2 的數(shù)據(jù)及所有后續(xù)數(shù)據(jù)的讀取狈谊。
- LStoreStore:確保 Store1 數(shù)據(jù)的寫(xiě)回先于 Store2 數(shù)據(jù)及所有后續(xù)數(shù)據(jù)的寫(xiě)回。
- LLoadStore:確保 Load1 數(shù)據(jù)的讀取先于 Store2 數(shù)據(jù)及所有后續(xù)數(shù)據(jù)的寫(xiě)回沟沙。
-
LStoreLoad:確保 Store1 數(shù)據(jù)的寫(xiě)回先于 Load2 數(shù)據(jù)及所有后續(xù)數(shù)據(jù)的讀取河劝。
JMM 插入內(nèi)存屏障保守策略:
在每個(gè) volatile 寫(xiě)操作的前面插入一個(gè) StoreStore 屏障,后面插入一個(gè) StoreLoad 屏障矛紫。
在每個(gè) volatile 讀操作的后面插入一個(gè) LoadLoad 屏障和一個(gè) LoadStore 屏障赎瞎。
image.png
由于 JMM插入內(nèi)存屏障保守策略增加了很多內(nèi)存屏障, 增加很多開(kāi)銷(xiāo)颊咬,性能會(huì)下降务甥,所以很多處理器對(duì)內(nèi)存屏障的插入進(jìn)行了優(yōu)化牡辽。如 X86/X64 處理器,只會(huì)在寫(xiě)操作后面增加一個(gè) storeLoad 內(nèi)存屏障敞临。
image.png