成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

詳解 Java 并發(fā)編程 volatile 關(guān)鍵字

開(kāi)發(fā)
Volatile被稱(chēng)之為輕量級(jí)的synchronized,即通過(guò)無(wú)鎖的方式保證可見(jiàn)性,而本文將通過(guò)自頂向下的方式深入剖析這個(gè)關(guān)鍵字的底層實(shí)現(xiàn),希望對(duì)你有幫助。

一、詳解volatile關(guān)鍵字

1. 普通變量并發(fā)操作的不可見(jiàn)性

我們編寫(xiě)一段多線(xiàn)程讀寫(xiě)一個(gè)變量的代碼,t1一旦感知num被t2修改,就會(huì)結(jié)束循環(huán),然而事實(shí)卻是這段代碼即使在t2完成修改之后,t1也像是感知不到變化一樣一直無(wú)限循環(huán)阻塞著:

private static int num = 0;

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(2);

        Thread t1 = new Thread(() -> {
            while (num == 0) {

            }
            log.info("num已被修改為:1");
            countDownLatch.countDown();
        });


        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            num++;
            log.info("t2修改num為1");
            countDownLatch.countDown();
        });

        t1.start();
        t2.start();
        countDownLatch.await();

        log.info("執(zhí)行結(jié)束");
    }

2. 最低線(xiàn)程安全和64位變量的風(fēng)險(xiǎn)

針對(duì)上述的情況,多線(xiàn)程在沒(méi)有正確的同步情況下,可能拿到一個(gè)失效的變量值,但它并非是沒(méi)有任何修改操作,我們稱(chēng)這種變量為最低線(xiàn)程安全,當(dāng)然這種概念也僅僅是針對(duì)一些例如int這樣的基本類(lèi)型。

若是64位例如double和long,因?yàn)镴MM內(nèi)存模型上規(guī)定了該變量操作在不同的處理器上進(jìn)行運(yùn)算操作,這就是的64位操作無(wú)法保證原子性,更談不上最低線(xiàn)程安全性了。

3. 通過(guò)volatile修飾保證可見(jiàn)性

于是我們將代碼增一個(gè)本文所引出的關(guān)鍵字volatile 加以修飾:

private volatile static int num = 0;

對(duì)應(yīng)的我們給出輸出結(jié)果,如預(yù)期一樣線(xiàn)程修改完之后線(xiàn)程1就會(huì)感知到變化而結(jié)束循環(huán),由此可知volatile關(guān)鍵字的第一個(gè)語(yǔ)義——保證并發(fā)場(chǎng)景下共享變量的可見(jiàn)性:

23:54:04.040 [Thread-0] INFO MultiApplication - num已被修改為:1
23:54:04.040 [Thread-1] INFO MultiApplication - t2修改num為1
23:54:04.042 [main] INFO MultiApplication - 執(zhí)行結(jié)束

4. 基于JMM模型詳解volatile的可見(jiàn)性

實(shí)際上,volatile底層實(shí)現(xiàn)和JMM內(nèi)存模型規(guī)范息息相關(guān),該模型規(guī)范了線(xiàn)程的本地變量(各個(gè)線(xiàn)程拿到共享變量num的副本)和主存(內(nèi)存中的變量num)的關(guān)系,其規(guī)范通過(guò)happens-before等規(guī)約強(qiáng)制規(guī)范了JVM需要針對(duì)這幾個(gè)原則要求做出相應(yīng)的處理來(lái)配合處理器保證共享變量操作的可見(jiàn)性和有序性。

這就要求t1和t2修改num的時(shí)候,都必須從主存中先加載才能進(jìn)行修改,以上述代碼為例,假設(shè)t1修改了num的值,完成后就必須將最新的結(jié)果寫(xiě)回主存中,而t2收到這個(gè)修改的通知后必須從主內(nèi)存中拉取最新的結(jié)果才能進(jìn)行操作:

關(guān)于JMM更多知識(shí),感興趣的讀者可以看看筆者這篇文章:《詳解 JMM 內(nèi)存模型》。

上述這個(gè)流程只是JMM模型的抽象,也就是JVM便于讓程序員理解的一種抽象模型而實(shí)際的落地, 所以為了更好理解volatile關(guān)鍵字修飾的變量,我們還是以上述的例子了解一下:

private volatile static int num = 0;

    public static void main(String[] args) throws InterruptedException {
        num = 24;
        num++;
    }

對(duì)應(yīng)的我們給出JIT后的匯編碼:

#  num = 24;
0x000000000368cd76: mov $0x18,%edi # 將24加載到edi寄存器
0x000000000368cd7b: mov %edi,0x68(%rsi) # 將edi寄存器的值存儲(chǔ)到內(nèi)存地址為[rsi + 0x68] 也就是變量num
0x000000000368cd7e: lock addl $0x0,(%rsp)  ;*putstatic num
                                           ; - org.example.Main::main@2 (line 13) # lock前綴起到類(lèi)似內(nèi)存屏障的作用,保證num=24這個(gè)寫(xiě)操作對(duì)內(nèi)存中所有的處理器可見(jiàn)
                                           
# num++;                                     
0x000000000368cd83: mov 0x68(%rsi),%edi  ;*getstatic num
                                         ; - org.example.Main::main@5 (line 14) # 將num值加載到edi寄存器
0x000000000368cd86: inc %edi # 基于increase將寄存器上的值也就是24加上1
0x000000000368cd88: mov %edi,0x68(%rsi) # 將edi寄存器上的值賦值給num
0x000000000368cd8b: lock addl $0x0,(%rsp)  ;*putstatic num
                                           ; - org.example.Main::main@10 (line 14) # 基于lock前綴實(shí)現(xiàn)JMM規(guī)范中的寫(xiě)回主存中,保證所有線(xiàn)程可見(jiàn)

針對(duì)num賦值為24這操作,匯編指令執(zhí)行了如下三步:

  • 通過(guò)mov $0x18,%edi將24(0x18)加載到edi寄存器。
  • mov %edi,0x68(%rsi)將這個(gè)24復(fù)制給num。
  • 重點(diǎn)來(lái)了,num=24即位于main 23行的代碼,它的字節(jié)碼為putstatic num這步本質(zhì)就是完成變量的賦值,實(shí)際上在完成變量賦值之后,它通過(guò)lock前綴指令起到一個(gè)內(nèi)存屏障的作用,保證上述的賦值操作對(duì)于所有的處理器可見(jiàn),也就是實(shí)現(xiàn)JMM規(guī)范中的寫(xiě)入主存操作(下文會(huì)從硬件層面分析該指令),由此保證num++操作時(shí)會(huì)先通過(guò)getstatic 到主存中獲取最新值到本地內(nèi)存中完成自增操作。

同樣的num++也是同理,可以看到對(duì)應(yīng)注釋的匯編碼,在完成自增即inc 操作后,同樣執(zhí)行l(wèi)ock前綴指令將數(shù)據(jù)寫(xiě)入主存。

5. 關(guān)于volatile可見(jiàn)性在硬件層面的分析

上文我們以JMM規(guī)范粗略的講解了lock前綴在規(guī)范層面上的可見(jiàn)性,查閱IA-32架構(gòu)軟件開(kāi)發(fā)者手冊(cè)可知,lock前綴的指令在多核處理器下會(huì)引發(fā)了兩件事情:

  • 將當(dāng)前變量num從當(dāng)前處理器的緩存行(cache-line)數(shù)據(jù)寫(xiě)回內(nèi)存。
  • 此時(shí),硬件層面上執(zhí)行當(dāng)前的CPU會(huì)通知其他處理器該變量已被修改,其他處理器cache-line中的num值全部變?yōu)閕nvalid(無(wú)效)。

這也就是我們Intel 64著名的MESI協(xié)議,將該實(shí)現(xiàn)代入我們的代碼,假設(shè)線(xiàn)程1的num被CPU-0的處理,線(xiàn)程2被CPU-1處理,實(shí)際上底層的實(shí)現(xiàn)是:

  • t1獲取共享變量num的值,此時(shí)并沒(méi)有其他核心上的線(xiàn)程獲取,狀態(tài)為E(exclusive)。
  • t2啟動(dòng)也獲取到num的值,此時(shí)總線(xiàn)嗅探到另一個(gè)CPU也有這個(gè)變量的緩存,所以?xún)蓚€(gè)CPU緩存行都設(shè)置為S(shard)。
  • t2修改num的值,通過(guò)總線(xiàn)嗅探機(jī)制發(fā)起通知,t1的線(xiàn)程收到消息后,將緩存行變量設(shè)置為I(invalid)。
  • t1需要輸出結(jié)果,因?yàn)榭吹阶约鹤兞渴菬o(wú)效的,于是通知總線(xiàn)讓t1將結(jié)果寫(xiě)回內(nèi)存,自己重新加載。

更多關(guān)于MESI協(xié)議的實(shí)現(xiàn)細(xì)節(jié),感興趣的讀者可以參考筆者的這篇文章:《CPU 緩存一致性問(wèn)題深度解析

volatile無(wú)法保證原子性

我們不妨看看下面這段代碼,首先我們需要了解一下的:

private static volatile int num;

    public static void main(String[] args) throws InterruptedException {
        num++;
    }

因?yàn)檫@段代碼位于筆者IDE的13行,基于該信息筆者拿到對(duì)應(yīng)的字節(jié)碼,可以看到num++這個(gè)操作在底層實(shí)現(xiàn)如下,大體來(lái)說(shuō)分為三步:

  • GETSTATIC 讀取num的值推到棧頂。
  • ICONST_1將常量1壓入操作數(shù)棧。
  • IADD將棧頂?shù)膎um和1進(jìn)行相加。

寫(xiě)回內(nèi)存中PUTSTATIC 寫(xiě)回主存。

LINENUMBER 13 L0
    GETSTATIC org/example/Main.num : I
    ICONST_1
    IADD
    PUTSTATIC org/example/Main.num : I

更進(jìn)一步,基于jitwatch,我們看到的對(duì)應(yīng)的匯編碼如下,同樣可以看到讀取、自增、寫(xiě)回操作:

0x00000000038ca096: mov 0x68(%r10),%r8d
0x00000000038ca09a: inc %r8d
0x00000000038ca09d: mov %r8d,0x68(%r10)

很明顯一個(gè)自增操作是由多條指令完成,這也就意味著,在上述指令執(zhí)行期間,很可能出現(xiàn)其他線(xiàn)程讀取到自增后但是還未寫(xiě)到內(nèi)存的過(guò)期值:

這里蠻補(bǔ)充一句,關(guān)于jitwatch的安裝使用,感興趣的讀者可以參考這篇文章:《初探 JITWatch 從零開(kāi)始的流程優(yōu)化之旅

我們查看代碼的運(yùn)行結(jié)果,可以看到最終的值不一定是10000,由此可以得出volatile并不能保證原子性:

public class VolatoleAdd {
    private static int num = 0;


   public void increase() {
        num++;
    }


    public static void main(String[] args) {

        int size = 10000;
        CountDownLatch downLatch = new CountDownLatch(1);
        ExecutorService threadPool = Executors.newFixedThreadPool(size);
        VolatoleAdd volatoleAdd = new VolatoleAdd();
        for (int i = 0; i < size; i++) {
            threadPool.submit(() -> {
                try {
                    downLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                volatoleAdd.increase();


            });
        }

        downLatch.countDown();
        threadPool.shutdown();

        while (!threadPool.isTerminated()) {

        }

        System.out.println(VolatoleAdd.num);//9998

    }
}

而對(duì)應(yīng)的解決方案我們可以通過(guò)synchronized、原子類(lèi)、或者Lock相關(guān)實(shí)現(xiàn)類(lèi)解決問(wèn)題。

6. volatile如何禁止指令重排序

而volatile不僅可以保證可見(jiàn)性,還可以避免指令重排序,底層同樣是通過(guò)JMM規(guī)約,禁止特定編譯器進(jìn)行有風(fēng)險(xiǎn)的重排序,以及在生成字節(jié)序列時(shí)插入內(nèi)存屏障避免CPU重排序解決問(wèn)題。

我們不妨看一段雙重鎖校驗(yàn)的單例模式代碼,代碼如下所示可以看到經(jīng)過(guò)雙重鎖校驗(yàn)后,會(huì)進(jìn)行new Singleton();

public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {
    }

    public  static Singleton getUniqueInstance() {
        //先判斷對(duì)象是否已經(jīng)實(shí)例過(guò),沒(méi)有實(shí)例化過(guò)才進(jìn)入加鎖代碼
        if (uniqueInstance == null) {
            //類(lèi)對(duì)象加鎖
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

這一操作,這個(gè)對(duì)象創(chuàng)建的操作乍一看是原子性的,實(shí)際上編譯后再執(zhí)行的機(jī)器碼會(huì)將其分為3個(gè)動(dòng)作:

  • 為引用uniqueInstance分配內(nèi)存空間
  • 初始化uniqueInstance
  • uniqueInstance指向分配的內(nèi)存空間

所以如果沒(méi)有volatile 禁止指令重排序的話(huà),1、2、3的順序操作很可能變成1、3、2,進(jìn)而可能出現(xiàn)下面這種情況:

  • 線(xiàn)程1執(zhí)行步驟1分配內(nèi)存空間。
  • 線(xiàn)程1執(zhí)行步驟3讓引用指向這個(gè)內(nèi)存空間。
  • 線(xiàn)程2進(jìn)入邏輯判斷發(fā)現(xiàn)uniqueInstance不為空直接返回,導(dǎo)致外部操作異常。

極端情況下,這種情況可能導(dǎo)致線(xiàn)程2外部操作到的可能是未初始化的對(duì)象,導(dǎo)致一些業(yè)務(wù)上的操作異常:

所以針對(duì)這種情況,我們需要增加volatile 關(guān)鍵字讓禁止這種指令重排序:

private volatile  static Singleton uniqueInstance;

按照J(rèn)MM的happens-before原則volatile的變量的寫(xiě)操作, happens-before后續(xù)讀該變量的代碼,這就會(huì)使的volatile操作可能實(shí)現(xiàn)如下幾點(diǎn):

  • 第二個(gè)針對(duì)volatile寫(xiě)操作時(shí),不管第一個(gè)操作是任何操作,都不能發(fā)生重排序。
  • 第一個(gè)針對(duì)volatile讀的操作,后續(xù)volatile任何操作都不能重排序。
  • 第一個(gè)volatile寫(xiě)操作,后續(xù)volatile讀,不能進(jìn)行重排序。

基于這套規(guī)范,在編譯器生成字節(jié)碼時(shí),就會(huì)通過(guò)內(nèi)存屏障的方式告知處理器禁止特定的重排序:

  • 每個(gè)volatile寫(xiě)后插入storestore,讓第一個(gè)寫(xiě)優(yōu)先于第二個(gè)寫(xiě),避免重排序后的寫(xiě)(可以理解未變量計(jì)算)順序重排序?qū)е碌挠?jì)數(shù)結(jié)果異常。
  • 每個(gè)volatile寫(xiě)后插入storeload,讓第一個(gè)寫(xiě)先于后續(xù)讀,避免讀取異常。
  • 每個(gè)volatile讀后加個(gè)loadstore,讓第一個(gè)讀操作先于第二個(gè)寫(xiě),避免讀寫(xiě)重排序的異常。
  • 每個(gè)volatile讀后加個(gè)loadload,讓第一個(gè)讀先于第二個(gè)讀,避免讀取順序重排序的異常。

回過(guò)頭來(lái),對(duì)于內(nèi)存屏障的實(shí)現(xiàn),以我們的單例模式初始化對(duì)象實(shí)例來(lái)說(shuō),其硬件架構(gòu)的實(shí)現(xiàn)上,這個(gè)new的操作涉及多條指令,在處理器執(zhí)行時(shí)可能會(huì)不按照規(guī)定順序交由不同的電路單元執(zhí)行,這就可能出現(xiàn)上述所謂1、3、2的情況。

對(duì)應(yīng)的我們給出相應(yīng)的匯編指令,可能看到其核心執(zhí)行步驟為如下三步:

  • 調(diào)用JVM內(nèi)部函數(shù),在堆內(nèi)存上分配Singleton內(nèi)存并完成對(duì)象創(chuàng)建,也就是在堆內(nèi)存中創(chuàng)建單例instance對(duì)象。
  • 獲取靜態(tài)變量存儲(chǔ)位置到r11上,即將元空間的靜態(tài)變量instance放到寄存器上為后續(xù)將步驟1所new的對(duì)象分配給該引用做好準(zhǔn)備。
  • 通過(guò)cmpxchg 源自指令比對(duì)r11對(duì)應(yīng)的引用instance是否為null,若為null則說(shuō)明沒(méi)有被其他線(xiàn)程初始化過(guò),則將r10創(chuàng)建的對(duì)象分配到該引用上,同時(shí)基于lock前綴將該引用的最近創(chuàng)建結(jié)果寫(xiě)入內(nèi)存,交由CPU硬件層面的MESI協(xié)議讓其他處理器可以看到最新結(jié)果。

對(duì)于避免指令重排序的語(yǔ)義,我們同第三條指令就能理解,即lock需要將更新操作寫(xiě)入內(nèi)存這一特性,保證lock前綴之上的步驟1和步驟2的操作都必須完成之后,才能執(zhí)行原子性的將創(chuàng)建的對(duì)象賦值給靜態(tài)變量instance的操作,即通過(guò)硬件層面的lock前綴保證有數(shù)據(jù)的情況下才能完成對(duì)象復(fù)制,從而形成一種指令無(wú)法超越內(nèi)存屏障的效果,由此具備避免指令重排序的語(yǔ)義:

# 調(diào)用JVM內(nèi)部函數(shù),在堆內(nèi)存上分配Singleton內(nèi)存并完成對(duì)象創(chuàng)建
0x0000000003d9300f: callq 0x00000000039057a0  ; OopMap{off=372}
                                              ;*new  ; - org.example.Singleton::getUniqueInstance@17 (line 16)
                                              ;   {runtime_call}
                                                                                           
0x0000000003d93014: int3  ;*new  ; - org.example.Singleton::getUniqueInstance@17 (line 16)
# 獲取靜態(tài)變量存儲(chǔ)位置到r11上,即將元空間的靜態(tài)變量instance放到寄存器上
             L0009: movabs $0x76b95d828,%r11  ;   {oop(a 'java/lang/Class' = 'org/example/Singleton')}
#  保證上述操作完成后,通過(guò)cmpxchg 源自指令比對(duì)r11對(duì)應(yīng)的引用instance是否為null,若為null則說(shuō)明沒(méi)有被其他線(xiàn)程初始化過(guò),則將r10創(chuàng)建的對(duì)象分配到該引用上,同時(shí)基于lock前綴做到一個(gè)類(lèi)似內(nèi)存屏障的作用,由此避免指令重排序
0x0000000003d9301f: lock cmpxchg %r10,(%r11)
# 執(zhí)行后續(xù)操作

二、關(guān)于volatile一些更進(jìn)一步的理解

1. volatile在并發(fā)場(chǎng)景中的性能表現(xiàn)和運(yùn)用

關(guān)于volatile性能的討論,實(shí)際上在jdk8以上synchronized 關(guān)鍵字的鎖升級(jí)的優(yōu)化機(jī)制上很說(shuō)明兩者的差異,我們大體只能得出如下三個(gè)結(jié)論:

  • 相較于普通變量num和加上volatile修飾后的普通變量num,因?yàn)楹笳叽嬖谝恢滦詥?wèn)題需要lock前綴寫(xiě)回主存,所以后者性能表現(xiàn)比普通變量表現(xiàn)差。
  • 對(duì)于單線(xiàn)程修改,多線(xiàn)程讀取并發(fā)共享變量的場(chǎng)景,我們更建議使用volatile,盡可能避免高并發(fā)場(chǎng)景下單修改多讀取變量的重量級(jí)鎖開(kāi)銷(xiāo)。
  • 對(duì)于并發(fā)修改,建議使用volatile配合鎖來(lái)保證可見(jiàn)性和數(shù)據(jù)一致性。

2. volatile與并發(fā)編程中三個(gè)重要特性

即原子性、有序性、可見(jiàn)性:

  • 原子性:一組操作要么全部都完成,要么全部失敗,Java就是基于synchronized或者各種Lock實(shí)現(xiàn)原則性。
  • 可見(jiàn)性:線(xiàn)程對(duì)于某些變量的操作,對(duì)于后續(xù)操作該變量的線(xiàn)程是立即可見(jiàn)的。Java基于synchronized或者各種Lock、volatile實(shí)現(xiàn)可見(jiàn)性,例如聲明volatile變量這就意味著Java代碼在操作該變量時(shí)每次都會(huì)從主內(nèi)存中加載。
  • 有序性:指令重排序只能保證串行語(yǔ)義一致性,并不能保證多線(xiàn)程情況下也一致,Java常常使用volatile禁止指令進(jìn)行重排序優(yōu)化。

三、小結(jié)

至此我們從幾個(gè)簡(jiǎn)單的實(shí)踐案例和volatile底層匯編碼等多個(gè)角度為該關(guān)鍵字進(jìn)行深入分析,希望對(duì)你有幫助。

責(zé)任編輯:趙寧寧 來(lái)源: 寫(xiě)代碼的SharkChili
相關(guān)推薦

2023-06-26 08:02:34

JSR重排序volatile

2011-06-14 13:26:27

volatile

2016-09-19 21:53:30

Java并發(fā)編程解析volatile

2021-03-10 15:59:39

JavaSynchronize并發(fā)編程

2022-06-29 08:05:25

Volatile關(guān)鍵字類(lèi)型

2019-09-04 14:14:52

Java編程數(shù)據(jù)

2011-06-21 09:50:51

volatile

2009-06-29 18:14:23

Java多線(xiàn)程volatile關(guān)鍵字

2022-08-17 07:53:10

Volatile關(guān)鍵字原子性

2013-01-30 10:12:14

Pythonyield

2009-09-02 09:24:03

C# this關(guān)鍵字

2020-07-17 20:15:03

架構(gòu)JMMvolatile

2018-01-19 10:43:06

Java面試官volatile關(guān)鍵字

2021-01-05 10:26:50

鴻蒙Javafinal

2022-11-12 18:32:50

Golangomitemptyjson

2021-02-01 13:10:07

Staticc語(yǔ)言UNIX系統(tǒng)

2020-11-11 08:45:48

Java

2017-05-27 20:59:30

Java多線(xiàn)程synchronize

2024-03-15 08:18:25

volatileAtomic關(guān)鍵字

2021-07-27 07:31:16

單例模式關(guān)鍵字
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 日本人做爰大片免费观看一老师 | 日本一区二区三区四区 | 久热精品在线 | 久久亚洲春色中文字幕久久久 | 操久久 | 国产亚洲成av人片在线观看桃 | 久久久久久久99 | www国产成人免费观看视频,深夜成人网 | 一区二区在线观看免费视频 | 欧美久久大片 | 美女黄18岁以下禁止观看 | 亚洲精品国产第一综合99久久 | 羞羞视频网站免费观看 | 综合国产 | 日本福利一区 | 精品国产视频 | 免费视频一区二区三区在线观看 | 国产电影一区二区三区爱妃记 | 免费人成激情视频在线观看冫 | 久久精品国产清自在天天线 | 久久精品网 | 91xxx在线观看 | 中文字幕免费中文 | 国产成人综合在线 | 四虎影院在线免费观看 | 国精久久| 久久性色 | 国产成人福利视频在线观看 | 欧美综合精品 | 精品一区二区三区四区 | 亚洲一区在线日韩在线深爱 | 成人黄色电影在线观看 | 久久精片| 99久久日韩精品免费热麻豆美女 | 91免费看片 | 午夜丁香视频在线观看 | 九九免费视频 | 请别相信他免费喜剧电影在线观看 | 99国内精品久久久久久久 | 九九亚洲| 国产高清视频在线观看播放 |