欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解Java volatile 內(nèi)存屏障底層原理語義

 更新時(shí)間:2021年09月24日 08:51:08   作者:沒頭腦遇到不高興  
為了保證內(nèi)存可見性,java 編譯器在生成指令序列的適當(dāng)位置會(huì)插入內(nèi)存屏障指令來禁止特定類型的處理器重排序。為了實(shí)現(xiàn) volatile 內(nèi)存語義,JMM 會(huì)分別限制這兩種類型的重排序類型

一、volatile關(guān)鍵字介紹及底層原理

1.volatile的特性(內(nèi)存語義)

當(dāng)一個(gè)變量被定義成volatile之后,它將具備兩項(xiàng)特性:第一項(xiàng)是保證此變量對(duì)所有線程的可見性,這里的“可見性”是指當(dāng)一條線程修改了這個(gè)變量的值,新值對(duì)于其他線程來說是可以立即得知的。而普通變量并不能做到這一點(diǎn),普通變量的值在線程間傳遞時(shí)均需要通過主內(nèi)存來完成。比如,線程A修改一個(gè)普通變量的值,然后向主內(nèi)存進(jìn)行回寫,另外一條線程B在線程A回寫完成了之后再對(duì)主內(nèi)存進(jìn)行讀取操作,新變量值才會(huì)對(duì)線程B可見。

使用volatile變量的第二個(gè)語義是禁止指令重排序優(yōu)化,普通的變量僅會(huì)保證在該方法的執(zhí)行過程中所有依賴賦值結(jié)果的地方都能獲取到正確的結(jié)果,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致。因?yàn)樵谕粋€(gè)線程的方法執(zhí)行過程中無法感知到這點(diǎn),這就是Java內(nèi)存模型中描述的所謂“線程內(nèi)表現(xiàn)為串行的語義”(Within-Thread As-If-Serial Semantics)。

2.volatile底層原理

volatile關(guān)鍵字修飾的變量可以保證可見性與有序性,無法保證原子性。那么volatile關(guān)鍵字的底層原理是什么呢?我們可以通過查看Java代碼的匯編指令去看一下volatile的底層原理:查詢Java代碼的匯編指令需要設(shè)置JVM允許參數(shù):-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp;如果你的jdk版本小于等于8還要在jdk里面添加Hsdis插件,將該插件目錄里面的兩個(gè)文件(hsdis-amd64.dll,hsdis-i386.dll)復(fù)制到 %JAVA_HOME%\jre\bin\server 下,然后運(yùn)行你的Java程序,就可以看到控制臺(tái)里面一堆的匯編指令代碼輸出了。

public class Singleton {
    private volatile static Singleton myinstance;
 
    public static Singleton getInstance() {
        if (myinstance == null) {
            synchronized (Singleton.class) {
                if (myinstance == null) {
                    myinstance = new Singleton();//對(duì)象創(chuàng)建過程,本質(zhì)可以分文三步
                }
            }
        }
        return myinstance;
    }
 
    public static void main(String[] args) {
        Singleton.getInstance();
    }
}

上面所示是一段標(biāo)準(zhǔn)的雙鎖檢測(cè)(Double Check Lock,DCL)單例代碼,可以觀察加入volatile和未加入volatile關(guān)鍵字時(shí)所生成的匯編代碼的差別。不加volatile關(guān)鍵字時(shí)在控制臺(tái)輸出指令搜索myinstance可以看到如下兩行

0x00000000038064dd: mov %r10d,0x68(%rsi)
0x00000000038064e1: shr $0x9,%rsi
0x00000000038064e5: movabs $0xf1d8000,%rax
0x00000000038064ef: movb $0x0,(%rsi,%rax,1) ;*putstatic myinstance
; - com.it.edu.jmm.Singleton::getInstance@24 (line 22)

加了volatile關(guān)鍵字后,變成下面這樣了:

0x0000000003cd6edd: mov %r10d,0x68(%rsi)
0x0000000003cd6ee1: shr $0x9,%rsi
0x0000000003cd6ee5: movabs $0xf698000,%rax
0x0000000003cd6eef: movb $0x0,(%rsi,%rax,1)
0x0000000003cd6ef3: lock addl $0x0,(%rsp) ;*putstatic myinstance
; - com.it.edu.jmm.Singleton::getInstance@24 (line 22)

通過對(duì)比發(fā)現(xiàn),關(guān)鍵變化在于有volatile修飾的變量,賦值后(前面movb $0x0,(%rsi,%rax,1)這句便是賦值操作)多執(zhí)行了一個(gè)“l(fā)ock addl $0x0,(%rsp)”操作,這個(gè)操作的作用相當(dāng)于一個(gè)內(nèi)存屏障(Memory Barrier或Memory Fence,指重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置,只有一個(gè)處理器訪問內(nèi)存時(shí),并不需要內(nèi)存屏障;但如果有兩個(gè)或更多處理器訪問同一塊內(nèi)存,且其中有一個(gè)在觀測(cè)另一個(gè),就需要內(nèi)存屏障來保證一致性了。

這里的關(guān)鍵在于lock前綴,它的作用是將本處理器的緩存寫入了內(nèi)存,該寫入動(dòng)作也會(huì)引起別的處理器或者別的內(nèi)核無效化(Invalidate,MESI協(xié)議的I狀態(tài))其緩存,這種操作相當(dāng)于對(duì)緩存中的變量做了一次前面介紹Java內(nèi)存模式中所說的“store和write”操作。所以通過這樣一個(gè)操作,可讓前面volatile變量的修改對(duì)其他處理器立即可見。lock指令的更底層實(shí)現(xiàn):如果支持緩存行會(huì)加緩存鎖(MESI);如果不支持緩存鎖,會(huì)加總線鎖。

二、volatile——可見性

volatile修飾變量之后,可以保證可見性,下面通過一個(gè)程序示例演示一下:

public class VolatileVisibilitySample {
    private volatile boolean initFlag = false;
    static Object object = new Object();
 
    public void refresh(){
        this.initFlag = true;
        System.out.println("線程:"+Thread.currentThread().getName()+":修改共享變量initFlag");
    }
 
    public void load(){
        int i = 0;
        while (!initFlag){
//            synchronized (object){
//                i++;
//            }
        }
        System.out.println("線程:"+Thread.currentThread().getName()+"當(dāng)前線程嗅探到initFlag的狀態(tài)的改變"+i);
    }
 
    public static void main(String[] args) throws InterruptedException {
        VolatileVisibilitySample sample = new VolatileVisibilitySample();
        Thread threadA = new Thread(()->{
            sample.refresh();
        },"threadA");
 
        Thread threadB = new Thread(()->{
            sample.load();
        },"threadB");
 
        threadB.start();
        Thread.sleep(2000);
        threadA.start();
    }
}

可以看到共享變量被volatile修飾之前,線程B中調(diào)用的方法中 “當(dāng)前線程嗅探到initFlag的狀態(tài)的改變” 這句輸出是打印不出來的,也就意味著線程A中將initFlag改為true,但是線程B并沒有獲取到最新值,程序一直在循環(huán)空跑。此時(shí)JMM操作如下圖:雖然線程A中將initFlag改為了true并且最終會(huì)同步回主內(nèi)存,但是線程B中循環(huán)讀取的initFlag一直都是從工作內(nèi)存讀取的,所以會(huì)一直進(jìn)行死循環(huán)無法退出。

添加了volatile修飾之后,“當(dāng)前線程嗅探到initFlag的狀態(tài)的改變” 這句話就會(huì)被打印出來,因?yàn)樘砑觱olatile關(guān)鍵字后,就會(huì)有l(wèi)ock指令,使用緩存一致性協(xié)議,線程B中會(huì)一直嗅探initFlag是否被改變,線程A修改initFlag后會(huì)立即同步回主內(nèi)存,這時(shí)候會(huì)通知線程B將緩存行狀態(tài)改為I(無效狀態(tài)),需要重新從主內(nèi)存讀取。如下圖所示:

我們將上面的代碼的load()方法進(jìn)行修改——去掉volatile關(guān)鍵字,添加synchronized同步塊,即修改為下面這樣的情況,會(huì)達(dá)到跟添加volatile關(guān)鍵字相同的效果,這是因?yàn)樘砑恿随i同步塊,CPU會(huì)分配時(shí)間片,線程進(jìn)行鎖競(jìng)爭導(dǎo)致線程上下文切換,重新讀取主存的變量。

public void load(){
        int i = 0;
        while (!initFlag){
            synchronized (object){
                i++;
            }
        }
        System.out.println("線程:"+Thread.currentThread().getName()+"當(dāng)前線程嗅探到initFlag的狀態(tài)的改變"+i);
    }

三、volatile——無法保證原子性

由于volatile變量只能保證可見性,在不符合以下兩條規(guī)則的運(yùn)算場(chǎng)景中,我們?nèi)匀灰ㄟ^加鎖(使用synchronized、java.util.concurrent中的鎖或原子類)來保證原子性:

  1. 運(yùn)算結(jié)果并不依賴變量的當(dāng)前值,或者能夠確保只有單一的線程修改變量的值。
  2. 變量不需要與其他的狀態(tài)變量共同參與不變約束

下面通過一個(gè)示例演示一下:10個(gè)線程,每個(gè)線程加1000次(counter++不是一個(gè)原子性的操作,可以通過javap命令查看底層指令,可以看到有加載變量數(shù)據(jù)、將變量放到操作數(shù)棧頂、執(zhí)行加法運(yùn)算等操作)。運(yùn)行幾次發(fā)現(xiàn),有時(shí)運(yùn)行結(jié)果是小于10000的。下面分析一下:

  • 1.首先counter不加volatile修飾時(shí):因?yàn)?0個(gè)線程同時(shí)對(duì)變量進(jìn)行自加1運(yùn)算,每個(gè)運(yùn)算一次后去寫會(huì)主內(nèi)存,會(huì)覆蓋其他線程的運(yùn)算結(jié)果,所以運(yùn)行結(jié)果可能會(huì)小于10000。
  • 2.counter添加volatile修飾時(shí):添加volatile修飾之后,變量被修改后會(huì)立即同步回主存,一直嗅探其他線程是否對(duì)變量進(jìn)行過修改,修改后重新從主存讀取變量。但是正因?yàn)樘砑恿藇olatile關(guān)鍵字時(shí)MESI緩存一致性協(xié)議生效了,當(dāng)一個(gè)變量執(zhí)行加1操作后,需要同步回主存,這是會(huì)鎖緩存行,通知其他線程變量已經(jīng)被修改過了,將本地緩存行改為I無效狀態(tài),這樣被改為無效狀態(tài)的線程本地加1操作的結(jié)果被丟棄了,沒有寫回主內(nèi)存,也就是白加了一次,所以運(yùn)行結(jié)果也可能會(huì)小于10000。

想要實(shí)現(xiàn)原子性操作,可以通過synchronized,ReentrantLock加鎖,或者使用AtomicInteger進(jìn)行原子性運(yùn)算。

public class VolatileAtomicSample {
    private static volatile int counter = 0;
 
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    counter++;
                }
            });
            thread.start();
        }
        Thread.sleep(1000);
        System.out.println(counter);
    }
}

四、volatile——禁止指令重排

1.指令重排

重排序是指編譯器和處理器為了優(yōu)化程序性能而對(duì)指令序列進(jìn)行重新排序的一種手段。java語言規(guī)范規(guī)定JVM線程內(nèi)部維持順序化語義。即只要程序的最終結(jié)果與
它順序化情況的結(jié)果相等,那么指令的執(zhí)行順序可以與代碼順序不一致,此過程叫指令的重排序。指令重排序的意義是什么?JVM能根據(jù)處理器特性(CPU多級(jí)緩存系統(tǒng)、多核處理器等)適當(dāng)?shù)膶?duì)機(jī)器指令進(jìn)行重排序,使機(jī)器指令能更符合CPU的執(zhí)行特性,最大限度的發(fā)揮機(jī)器性能。

下圖為從源碼到最終執(zhí)行的指令序列示意圖

指令重排主要有兩個(gè)階段:

1.編譯器編譯階段:編譯器加載class文件編譯為機(jī)器碼時(shí)進(jìn)行指令重排

2.CPU執(zhí)行階段: CPU執(zhí)行匯編指令時(shí),可能會(huì)對(duì)指令進(jìn)行重排序

2.as-if-serial語義

as-if-serial語義的意思是:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。為了遵守as-if-serial語義,編譯器和處理器不會(huì)對(duì)存在數(shù)據(jù)依賴關(guān)系的操作做重排序,因?yàn)檫@種重排序會(huì)改變執(zhí)行結(jié)果。但是,如果操作之間不存在數(shù)據(jù)依賴關(guān)系,這些操作就可能被編譯器和處理器重排序。

通過一個(gè)程序代碼,演示一下指令重排的效果:只有x=0并且y=0的情況下才會(huì)跳出循環(huán)

public class VolatileReOrderSample {
    private static int x = 0, y = 0;
    private static int a = 0, b =0;
    static Object object = new Object();
 
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
 
        for (;;){
            i++;
            x = 0; y = 0;
            a = 0; b = 0;
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 1; 
                    x = b;
                }
            });
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    y = a;
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
 
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if(x == 0 && y == 0) {
                System.err.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }
    }
}

通過分析,會(huì)有三種可能的輸出:[0,1],[1,0],[1,1]。

  • 輸出可能1——[0,1]:線程1先執(zhí)行完,線程2再執(zhí)行,則會(huì)出現(xiàn)x=0,y=1
  • 輸出可能1——[1,0]:線程2先執(zhí)行完,線程1再執(zhí)行,則會(huì)出現(xiàn)x=1,y=0
  • 輸出可能1——[1,1]:線程1、線程2交替執(zhí)行,a=1,b=1,然后執(zhí)行x=1,y=1,則會(huì)出現(xiàn)x=1,y=1

當(dāng)運(yùn)行之后會(huì)發(fā)現(xiàn)上面分析的三種情況確實(shí)出現(xiàn)了,但是程序最終跳出了循環(huán),也就是出現(xiàn)了x=0并且y=0的情況,這說明出現(xiàn)了指令重排的情況,即線程1中a=1 x=b的指令出現(xiàn)了順序調(diào)整或線程2中b=1 y=a的指令出現(xiàn)了順序調(diào)整。

當(dāng)我們給變量a和b添加volatile關(guān)鍵字修飾后(private volatile static int a = 0, b =0;),再次運(yùn)行發(fā)現(xiàn)程序一直在循環(huán)輸出,沒有出現(xiàn)x=y=0的情況從而退出循環(huán)。

volatile可以禁止指令重排的原因是因?yàn)樘砑恿薼ock指令,會(huì)添加內(nèi)存屏障。

五、volatile與內(nèi)存屏障(Memory Barrier)

1.內(nèi)存屏障(Memory Barrier)

內(nèi)存屏障(Memory Barrier)又稱內(nèi)存柵欄,是一個(gè)CPU指令,它的作用有兩個(gè),一是保證特定操作的執(zhí)行順序,二是保證某些變量的內(nèi)存可見性(利用該特性實(shí)現(xiàn)volatile的內(nèi)存可見性)。由于編譯器和處理器都能執(zhí)行指令重排優(yōu)化。如果在指令間插入一條Memory Barrier則會(huì)告訴編譯器和CPU,不管什么指令都不能和這條Memory Barrier指令重排序,也就是說通過插入內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行重排序優(yōu)化。Memory Barrier的另外一個(gè)作用是強(qiáng)制刷出各種CPU的緩存數(shù)據(jù),因此任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本??傊瑅olatile變量正是通過內(nèi)存屏障(lock指令)實(shí)現(xiàn)其在內(nèi)存中的語義,即可見性和禁止重排優(yōu)化。

上面的程序示例:synchronized+volatile實(shí)現(xiàn)的DCL模式的單例模式,就是利用了volatile禁止指令重排的特性。因?yàn)閙yinstance = new Singleton();這句代碼本質(zhì)上是有三步:1.為對(duì)象分配內(nèi)存空間;2.實(shí)例化對(duì)象數(shù)據(jù);3.將引用指向?qū)ο髮?shí)例的內(nèi)存空間。如果第一個(gè)線程執(zhí)行創(chuàng)建對(duì)象時(shí)出現(xiàn)了指令重排,比如3排到了2之前,那么線程2在最外層代碼判斷myinstance!=null為true返回對(duì)象引用,但是實(shí)際上這時(shí)候?qū)ο笊形闯跏蓟瓿?,這樣是有問題的,需要通過添加volatile關(guān)鍵字去禁止指令重排。

2.volatile的內(nèi)存語義實(shí)現(xiàn)

前面提到過重排序分為編譯器重排序和處理器重排序。為了實(shí)現(xiàn)volatile內(nèi)存語義,JMM會(huì)分別限制這兩種類型的重排序類型。下圖是JMM針對(duì)編譯器制定的volatile重排序規(guī)則表。

舉例來說,第三行最后一個(gè)單元格的意思是:在程序中,當(dāng)?shù)谝粋€(gè)操作為普通變量的讀或?qū)憰r(shí),如果第二個(gè)操作為volatile寫,則編譯器不能重排序這兩個(gè)操作。
從上圖我們可以看出:

  • 當(dāng)?shù)诙€(gè)操作是volatile寫時(shí),不管第一個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保volatile寫之前的操作不會(huì)被編譯器重排序到volatile寫之后。
  • 當(dāng)?shù)谝粋€(gè)操作是volatile讀時(shí),不管第二個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保volatile讀之后的操作不會(huì)被編譯器重排序到volatile讀之前。
  • 當(dāng)?shù)谝粋€(gè)操作是volatile寫,第二個(gè)操作是volatile讀時(shí),不能重排序。

為了實(shí)現(xiàn)volatile的內(nèi)存語義,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。對(duì)于編譯器來說,發(fā)現(xiàn)一個(gè)最優(yōu)布置來最小化插入屏障的總數(shù)幾乎不可能。為此,JMM采取保守策略。下面是基于保守策略的JMM內(nèi)存屏障插入策略。

  • 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障。
  • 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障。
  • 在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障。
  • 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障。

上述內(nèi)存屏障插入策略非常保守,但它可以保證在任意處理器平臺(tái),任意的程序中都能得到正確的volatile內(nèi)存語義。

下面是保守策略下,volatile寫插入內(nèi)存屏障后生成的指令序列示意圖,如圖所示。

上圖中StoreStore屏障可以保證在volatile寫之前,其前面的所有普通寫操作已經(jīng)對(duì)任意處理器可見了。這是因?yàn)镾toreStore屏障將保障上面所有的普通寫在volatile寫之前刷新到主內(nèi)存。

而volatile寫后面的StoreLoad屏障,作用是避免volatile寫與后面可能有的volatile讀/寫操作重排序

下圖是在保守策略下,volatile讀插入內(nèi)存屏障后生成的指令序列示意圖

上圖中LoadLoad屏障用來禁止處理器把上面的volatile讀與下面的普通讀重排序。LoadStore屏障用來禁止處理器把上面的volatile讀與下面的普通寫重排序。

上述volatile寫和volatile讀的內(nèi)存屏障插入策略非常保守。在實(shí)際執(zhí)行時(shí),只要不改變 volatile寫-讀的內(nèi)存語義,編譯器可以根據(jù)具體情況省略不必要的屏障。

六、JMM對(duì)volatile的特殊規(guī)則定義

最后我們?cè)貸ava內(nèi)存模型中對(duì)volatile變量定義的特殊規(guī)則的定義。假定T表示一個(gè)線程,V和W分別表示兩個(gè)volatile型變量,那么在進(jìn)行read、load、use、assign、store和write操作時(shí)需要滿足如下規(guī)則:

只有當(dāng)線程T對(duì)變量V執(zhí)行的前一個(gè)動(dòng)作是load的時(shí)候,線程T才能對(duì)變量V執(zhí)行use動(dòng)作;并且,只有當(dāng)線程T對(duì)變量V執(zhí)行的后一個(gè)動(dòng)作是use的時(shí)候,線程T才能對(duì)變量V執(zhí)行l(wèi)oad動(dòng)作。線程T對(duì)變量V的use動(dòng)作可以認(rèn)為是和線程T對(duì)變量V的load、read動(dòng)作相關(guān)聯(lián)的,必須連續(xù)且一起出現(xiàn)。

這條規(guī)則要求在工作內(nèi)存中,每次使用V前都必須先從主內(nèi)存刷新最新的值,用于保證能看見其他線程對(duì)變量V所做的修改。

只有當(dāng)線程T對(duì)變量V執(zhí)行的前一個(gè)動(dòng)作是assign的時(shí)候,線程T才能對(duì)變量V執(zhí)行store動(dòng)作;并且,只有當(dāng)線程T對(duì)變量V執(zhí)行的后一個(gè)動(dòng)作是store的時(shí)候,線程T才能對(duì)變量V執(zhí)行assign動(dòng)作。線程T對(duì)變量V的assign動(dòng)作可以認(rèn)為是和線程T對(duì)變量V的store、write動(dòng)作相關(guān)聯(lián)的,必須連續(xù)且一起出現(xiàn)。

這條規(guī)則要求在工作內(nèi)存中,每次修改V后都必須立刻同步回主內(nèi)存中,用于保證其他線程可以看到自己對(duì)變量V所做的修改。

假定動(dòng)作A是線程T對(duì)變量V實(shí)施的use或assign動(dòng)作,假定動(dòng)作F是和動(dòng)作A相關(guān)聯(lián)的load或store動(dòng)作,假定動(dòng)作P是和動(dòng)作F相應(yīng)的對(duì)變量V的read或write動(dòng)作;與此類似,假定動(dòng)作B是線程T對(duì)變量W實(shí)施的use或assign動(dòng)作,假定動(dòng)作G是和動(dòng)作B相關(guān)聯(lián)的load或store動(dòng)作,假定動(dòng)作Q是和動(dòng)作G相應(yīng)的對(duì)變量W的read或write動(dòng)作。如果A先于B,那么P先于Q。

這條規(guī)則要求volatile修飾的變量不會(huì)被指令重排序優(yōu)化,從而保證代碼的執(zhí)行順序與程序的順序相同。

下一篇預(yù)告——并發(fā)編程三大特性:原子性,可見性,有序性,happen-before原則

到此這篇關(guān)于詳解Java volatile 內(nèi)存屏障底層原理語義的文章就介紹到這了,更多相關(guān)Java volatile 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論