<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          ?反制面試官 | 14張?jiān)韴D | 再也不怕被問 volatile!

          共 10268字,需瀏覽 21分鐘

           ·

          2020-10-14 15:50

          絮叨

          這一篇也算是Java并發(fā)編程的開篇,看了很多資料,但是輪到自己去整理去總結(jié)的時(shí)候,發(fā)現(xiàn)還是要多看幾遍資料才能完全理解。還有一個(gè)很重要的點(diǎn)就是,畫圖是加深印象和檢驗(yàn)自己是否理解的一個(gè)非常好的方法。

          一、Volatile怎么念?

          volatile怎么念

          看到這個(gè)單詞一直不知道怎么發(fā)音

          英?[?v?l?ta?l]??美?[?vɑ?l?tl]

          adj. [化學(xué)]?揮發(fā)性的;不穩(wěn)定的;爆炸性的;反復(fù)無(wú)常的

          那Java中volatile又是干啥的呢?

          二、Java中volatile用來(lái)干啥?

          • Volatile是Java虛擬機(jī)提供的輕量級(jí)的同步機(jī)制(三大特性)
            • 保證可見性
            • 不保證原子性
            • 禁止指令重排

          要理解三大特性,就必須知道Java內(nèi)存模型(JMM),那JMM又是什么呢?

          JMM是啥

          三、JMM又是啥?

          這是一份精心總結(jié)的Java內(nèi)存模型思維導(dǎo)圖,拿去不謝。

          原理圖1-Java內(nèi)存模型

          3.1 為什么需要Java內(nèi)存模型?

          ?

          Why:屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異

          ?

          JMM是Java內(nèi)存模型,也就是Java Memory Model,簡(jiǎn)稱JMM,本身是一種抽象的概念,實(shí)際上并不存在,它描述的是一組規(guī)則或規(guī)范,通過這組規(guī)范定義了程序中各個(gè)變量(包括實(shí)例字段,靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素)的訪問方式。

          3.2 到底什么是Java內(nèi)存模型?

          • 1.定義程序中各種變量的訪問規(guī)則
          • 2.把變量值存儲(chǔ)到內(nèi)存的底層細(xì)節(jié)
          • 3.從內(nèi)存中取出變量值的底層細(xì)節(jié)

          3.3 Java內(nèi)存模型的兩大內(nèi)存是啥?

          原理圖2-兩大內(nèi)存
          • 主內(nèi)存
            • Java堆中對(duì)象實(shí)例數(shù)據(jù)部分
            • 對(duì)應(yīng)于物理硬件的內(nèi)存
          • 工作內(nèi)存
            • Java棧中的部分區(qū)域
            • 優(yōu)先存儲(chǔ)于寄存器和高速緩存

          3.4 Java內(nèi)存模型是怎么做的?

          Java內(nèi)存模型的幾個(gè)規(guī)范:

          • 1.所有變量存儲(chǔ)在主內(nèi)存

          • 2.主內(nèi)存是虛擬機(jī)內(nèi)存的一部分

          • 3.每條線程有自己的工作內(nèi)存

          • 4.線程的工作內(nèi)存保存變量的主內(nèi)存副本

          • 5.線程對(duì)變量的操作必須在工作內(nèi)存中進(jìn)行

          • 6.不同線程之間無(wú)法直接訪問對(duì)方工作內(nèi)存中的變量

          • 7.線程間變量值的傳遞均需要通過主內(nèi)存來(lái)完成

          由于JVM運(yùn)行程序的實(shí)體是線程,而每個(gè)線程創(chuàng)建時(shí)JVM都會(huì)為其創(chuàng)建一個(gè)工作內(nèi)存(有些地方稱為??臻g),工作內(nèi)存是每個(gè)線程的私有數(shù)據(jù)區(qū)域,而Java內(nèi)存模型中規(guī)定所有變量都存儲(chǔ)在主內(nèi)存,主內(nèi)存是共享內(nèi)存區(qū)域,所有線程都可以訪問,但線程對(duì)變量的操作(讀取賦值等)必須在工作內(nèi)存中進(jìn)行,首先要將變量從主內(nèi)存拷貝到自己的工作內(nèi)存空間,然后對(duì)變量進(jìn)行操作,操作完成后再將變量寫會(huì)主內(nèi)存,不能直接操作主內(nèi)存中的變量,各個(gè)線程中的工作內(nèi)存中存儲(chǔ)著主內(nèi)存中的變量副本拷貝,因此不同的線程間無(wú)法訪問對(duì)方的工作內(nèi)存,線程間的通信(傳值)必須通過主內(nèi)存來(lái)完成,其簡(jiǎn)要訪問過程:

          原理圖3-Java內(nèi)存模型

          3.5 Java內(nèi)存模型的三大特性

          • 可見性(當(dāng)一個(gè)線程修改了共享變量的值時(shí),其他線程能夠立即得知這個(gè)修改)
          • 原子性(一個(gè)操作或一系列操作是不可分割的,要么同時(shí)成功,要么同時(shí)失?。?/section>
          • 有序性(變量賦值操作的順序與程序代碼中的執(zhí)行順序一致)

          關(guān)于有序性:如果在本線程內(nèi)觀察,所有的操作都是有序的;如果在一個(gè)線程中觀察另一個(gè)線程,所有的操作都是無(wú)序的。前半句是指“線程內(nèi)似表現(xiàn)為串行的語(yǔ)義”(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”現(xiàn)象和“工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象。

          四、能給個(gè)示例說下怎么用volatile的嗎?

          考慮一下這種場(chǎng)景:

          ?

          有一個(gè)對(duì)象的字段number初始化值=0,另外這個(gè)對(duì)象有一個(gè)公共方法setNumberTo100()可以設(shè)置number = 100,當(dāng)主線程通過子線程來(lái)調(diào)用setNumberTo100()后,主線程是否知道number值變了呢?

          答案:如果沒有使用volatile來(lái)定義number變量,則主線程不知道子線程更新了number的值。

          ?

          (1)定義如上述所說的對(duì)象:ShareData

          class?ShareData?{
          ????int?number?=?0;

          ????public?void?setNumberTo100()?{
          ????????this.number?=?100;
          ????}
          }

          (2)主線程中初始化一個(gè)子線程,名字叫做子線程

          子線程先休眠3s,然后設(shè)置number=100。主線程不斷檢測(cè)的number值是否等于0,如果不等于0,則退出主線程。

          public?class?volatileVisibility?{
          ????public?static?void?main(String[]?args)?{
          ????????//?資源類
          ????????ShareData?shareData?=?new?ShareData();

          ????????//?子線程?實(shí)現(xiàn)了Runnable接口的,lambda表達(dá)式
          ????????new?Thread(()?->?{

          ????????????System.out.println(Thread.currentThread().getName()?+?"\t?come?in");

          ????????????//?線程睡眠3秒,假設(shè)在進(jìn)行運(yùn)算
          ????????????try?{
          ????????????????TimeUnit.SECONDS.sleep(3);
          ????????????}?catch?(InterruptedException?e)?{
          ????????????????e.printStackTrace();
          ????????????}
          ????????????//?修改number的值
          ????????????myData.setNumberTo100();

          ????????????//?輸出修改后的值
          ????????????System.out.println(Thread.currentThread().getName()?+?"\t?update?number?value:"?+?myData.number);

          ????????},?"子線程").start();

          ????????while(myData.number?==?0)?{
          ????????????//?main線程就一直在這里等待循環(huán),直到number的值不等于零
          ????????}

          ????????//?按道理這個(gè)值是不可能打印出來(lái)的,因?yàn)橹骶€程運(yùn)行的時(shí)候,number的值為0,所以一直在循環(huán)
          ????????//?如果能輸出這句話,說明子線程在睡眠3秒后,更新的number的值,重新寫入到主內(nèi)存,并被main線程感知到了
          ????????System.out.println(Thread.currentThread().getName()?+?"\t?主線程感知到了?number?不等于?0");

          ????????/**
          ?????????*?最后輸出結(jié)果:
          ?????????*?子線程?????come?in
          ?????????*?子線程?????update?number?value:100
          ?????????*?最后線程沒有停止,并行沒有輸出"主線程知道了?number?不等于0"這句話,說明沒有用volatile修飾的變量,變量的更新是不可見的
          ?????????*/

          ????}
          }
          沒有使用volatile

          (3)我們用volatile修飾變量number

          class?ShareData?{
          ????//volatile?修飾的關(guān)鍵字,是為了增加多個(gè)線程之間的可見性,只要有一個(gè)線程修改了內(nèi)存中的值,其它線程也能馬上感知
          ????volatile?int?number?=?0;

          ????public?void?setNumberTo100()?{
          ????????this.number?=?100;
          ????}
          }

          輸出結(jié)果:

          子線程??come?in
          子線程??update?number?value:100
          main??主線程知道了?number?不等于?0

          Process?finished?with?exit?code?0
          mark

          「小結(jié):說明用volatile修飾的變量,當(dāng)某線程更新變量后,其他線程也能感知到。」

          五、那為什么其他線程能感知到變量更新?

          mark

          其實(shí)這里就是用到了“窺探(snooping)”協(xié)議。在說“窺探(snooping)”協(xié)議之前,首先談?wù)劸彺嬉恢滦缘膯栴}。

          5.1 緩存一致性

          當(dāng)多個(gè)CPU持有的緩存都來(lái)自同一個(gè)主內(nèi)存的拷貝,當(dāng)有其他CPU偷偷改了這個(gè)主內(nèi)存數(shù)據(jù)后,其他CPU并不知道,那拷貝的內(nèi)存將會(huì)和主內(nèi)存不一致,這就是緩存不一致。那我們?nèi)绾蝸?lái)保證緩存一致呢?這里就需要操作系統(tǒng)來(lái)共同制定一個(gè)同步規(guī)則來(lái)保證,而這個(gè)規(guī)則就有MESI協(xié)議。

          如下圖所示,CPU2 偷偷將num修改為2,內(nèi)存中num也被修改為2,但是CPU1和CPU3并不知道num值變了。

          原理圖4-緩存一致性1

          5.2 MESI

          當(dāng)CPU寫數(shù)據(jù)時(shí),如果發(fā)現(xiàn)操作的變量是共享變量,即在其它CPU中也存在該變量的副本,系統(tǒng)會(huì)發(fā)出信號(hào)通知其它CPU將該內(nèi)存變量的緩存行設(shè)置為無(wú)效。如下圖所示,CPU1和CPU3 中num=1已經(jīng)失效了。

          原理圖5-緩存一致性2

          當(dāng)其它CPU讀取這個(gè)變量的時(shí),發(fā)現(xiàn)自己緩存該變量的緩存行是無(wú)效的,那么它就會(huì)從內(nèi)存中重新讀取。

          如下圖所示,CPU1和CPU3發(fā)現(xiàn)緩存的num值失效了,就重新從內(nèi)存讀取,num值更新為2。

          原理圖6-緩存一致性3

          5.3 總線嗅探

          那其他CPU是怎么知道要將緩存更新為失效的呢?這里是用到了總線嗅探技術(shù)。

          每個(gè)CPU不斷嗅探總線上傳播的數(shù)據(jù)來(lái)檢查自己緩存值是否過期了,如果處理器發(fā)現(xiàn)自己的緩存行對(duì)應(yīng)的內(nèi)存地址被修改,就會(huì)將當(dāng)前處理器的緩存行設(shè)置為無(wú)效狀態(tài),當(dāng)處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候,會(huì)重新從內(nèi)存中把數(shù)據(jù)讀取到處理器緩存中。

          原理圖7-緩存一致性4

          5.4 總線風(fēng)暴

          總線嗅探技術(shù)有哪些缺點(diǎn)?

          由于MESI緩存一致性協(xié)議,需要不斷對(duì)主線進(jìn)行內(nèi)存嗅探,大量的交互會(huì)導(dǎo)致總線帶寬達(dá)到峰值。因此不要濫用volatile,可以用鎖來(lái)替代,看場(chǎng)景啦~

          六、能演示下volatile為什么不保證原子性嗎?

          原子性:一個(gè)操作或一系列操作是不可分割的,要么同時(shí)成功,要么同時(shí)失敗。

          「這個(gè)定義和volatile啥關(guān)系呀,完全不能理解呀?Show me the code!」

          考慮一下這種場(chǎng)景:

          ?

          當(dāng)20個(gè)線程同時(shí)給number自增1,執(zhí)行1000次以后,number的值為多少呢?

          ?

          在單線程的場(chǎng)景,答案是20000,如果是多線程的場(chǎng)景下呢?答案是可能是20000,但很多情況下都是小于20000。

          示例代碼:

          package?com.jackson0714.passjava.threads;

          /**
          ?演示volatile?不保證原子性
          ?*?@create:?2020-08-13?09:53
          ?*/


          public?class?VolatileAtomicity?{
          ????public?static?volatile?int?number?=?0;

          ????public?static?void?increase()?{
          ????????number++;
          ????}

          ????public?static?void?main(String[]?args)?{

          ????????for?(int?i?=?0;?i?50;?i++)?{
          ????????????new?Thread(()?->?{
          ????????????????for?(int?j?=?0;?j?1000;?j++)?{
          ????????????????????increase();
          ????????????????}
          ????????????},?String.valueOf(i)).start();
          ????????}

          ????????//?當(dāng)所有累加線程都結(jié)束
          ????????while(Thread.activeCount()?>?2)?{
          ????????????Thread.yield();
          ????????}

          ????????System.out.println(number);
          ????}
          }

          執(zhí)行結(jié)果:第一次19144,第二次20000,第三次19378。

          volatile第一次執(zhí)行結(jié)果
          volatile第二次執(zhí)行結(jié)果
          volatile第三次執(zhí)行結(jié)果

          我們來(lái)分析一下increase()方法,通過反編譯工具javap得到如下匯編代碼:

          ??public?static?void?increase();
          ????Code:
          ???????0:?getstatic?????#2??????????????????//?Field?number:I
          ???????3:?iconst_1
          ???????4:?iadd
          ???????5:?putstatic?????#2??????????????????//?Field?number:I
          ???????8:?return

          number++其實(shí)執(zhí)行了3條指令

          ?

          getstatic:拿number的原始值 iadd:進(jìn)行加1操作 putfield:把加1后的值寫回

          ?

          執(zhí)行了getstatic指令number的值取到操作棧頂時(shí),volatile關(guān)鍵字保證了number的值在此時(shí)是正確的,但是在執(zhí)行iconst_1、iadd這些指令的時(shí)候,其他線程可能已經(jīng)把number的值改變了,而操作棧頂?shù)闹稻妥兂闪诉^期的數(shù)據(jù),所以putstatic指令執(zhí)行后就可能把較小的number值同步回主內(nèi)存之中。

          總結(jié)如下:

          ?

          在執(zhí)行number++這行代碼時(shí),即使使用volatile修飾number變量,在執(zhí)行期間,還是有可能被其他線程修改,沒有保證原子性。

          ?

          七、怎么保證輸出結(jié)果是20000呢?

          7.1 synchronized同步代碼塊

          我們可以通過使用synchronized同步代碼塊來(lái)保證原子性。從而使結(jié)果等于20000

          public?synchronized?static?void?increase()?{
          ???number++;
          }
          synchronized同步代碼塊執(zhí)行結(jié)果

          但是使用synchronized太重了,會(huì)造成阻塞,只有一個(gè)線程能進(jìn)入到這個(gè)方法。我們可以使用Java并發(fā)包(JUC)中的AtomicInterger工具包。

          7.2 AtomicInterger原子性操作

          我們來(lái)看看AtomicInterger原子自增的方法getAndIncrement()

          AtomicInterger
          public?static?AtomicInteger?atomicInteger?=?new?AtomicInteger();

          public?static?void?main(String[]?args)?{

          ????for?(int?i?=?0;?i?20;?i++)?{
          ????????new?Thread(()?->?{
          ????????????for?(int?j?=?0;?j?1000;?j++)?{
          ????????????????atomicInteger.getAndIncrement();
          ????????????}
          ????????},?String.valueOf(i)).start();
          ????}

          ????//?當(dāng)所有累加線程都結(jié)束
          ????while(Thread.activeCount()?>?2)?{
          ????????Thread.yield();
          ????}

          ????System.out.println(atomicInteger);
          }

          多次運(yùn)行的結(jié)果都是20000。

          getAndIncrement的執(zhí)行結(jié)果

          八、禁止指令重排又是啥?

          說到指令重排就得知道為什么要重排,有哪幾種重排。

          如下圖所示,指令執(zhí)行順序是按照1>2>3>4的順序,經(jīng)過重排后,執(zhí)行順序更新為指令3->4->2->1。

          原理圖8-指令重排

          會(huì)不會(huì)感覺到重排把指令順序都打亂了,這樣好嗎?

          可以回想下小學(xué)時(shí)候的數(shù)學(xué)題:2+3-5=?,如果把運(yùn)算順序改為3-5+2=?,結(jié)果也是一樣的。所以指令重排是要保證單線程下程序結(jié)果不變的情況下做重排。

          8.1 為什么要重排

          計(jì)算機(jī)在執(zhí)行程序時(shí),為了提高性能,編譯器和處理器常常會(huì)對(duì)指令做重排序。

          8.2 有哪幾種重排

          • 1.編譯器優(yōu)化重排:編譯器在不改變單線程程序語(yǔ)義的前提下,可以重新安排語(yǔ)句的執(zhí)行順序。

          • 2.指令級(jí)的并行重排:現(xiàn)代處理器采用了指令級(jí)并行技術(shù)來(lái)將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。

          • 3.內(nèi)存系統(tǒng)的重排:由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。

          原理圖9-三種重排

          注意:

          • 單線程環(huán)境里面確保最終執(zhí)行結(jié)果和代碼順序的結(jié)果一致

          • 處理器在進(jìn)行重排序時(shí),必須要考慮指令之間的數(shù)據(jù)依賴性

          • 多線程環(huán)境中線程交替執(zhí)行,由于編譯器優(yōu)化重排的存在,兩個(gè)線程中使用的變量能否保證一致性是無(wú)法確定的,結(jié)果無(wú)法預(yù)測(cè)。

          8.3 舉個(gè)例子來(lái)說說多線程中的指令重排?

          設(shè)想一下這種場(chǎng)景:定義了變量num=0和變量flag=false,線程1調(diào)用初始化函數(shù)init()執(zhí)行后,線程調(diào)用add()方法,當(dāng)另外線程判斷flag=true后,執(zhí)行num+100操作,那么我們預(yù)期的結(jié)果是num會(huì)等于101,但因?yàn)橛兄噶钪嘏诺目赡?,num=1和flag=true執(zhí)行順序可能會(huì)顛倒,以至于num可能等于100

          public?class?VolatileResort?{
          ????static?int?num?=?0;
          ????static?boolean?flag?=?false;
          ????public?static?void?init()?{
          ????????num=?1;
          ????????flag?=?true;
          ????}
          ????public?static?void?add()?{
          ????????if?(flag)?{
          ????????????num?=?num?+?5;
          ????????????System.out.println("num:"?+?num);
          ????????}
          ????}
          ????public?static?void?main(String[]?args)?{
          ????????init();
          ????????new?Thread(()?->?{
          ????????????add();
          ????????},"子線程").start();
          ????}
          }

          先看線程1中指令重排:

          num= 1;flag = true; 的執(zhí)行順序變?yōu)?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">flag=true;num = 1;,如下圖所示的時(shí)序圖

          原理圖10-線程1指令重排

          如果線程2 num=num+5 在線程1設(shè)置num=1之前執(zhí)行,那么線程2的num變量值為5。如下圖所示的時(shí)序圖。

          原理圖11-線程2在num=1之前執(zhí)行

          8.4 volatile怎么實(shí)現(xiàn)禁止指令重排?

          我們使用volatile定義flag變量:

          static?volatile?boolean?flag?=?false;

          「如何實(shí)現(xiàn)禁止指令重排:」

          原理:在volatile生成的指令序列前后插入內(nèi)存屏障(Memory Barries)來(lái)禁止處理器重排序。

          「有如下四種內(nèi)存屏障:」

          四種內(nèi)存屏障

          「volatile寫的場(chǎng)景如何插入內(nèi)存屏障:」

          • 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障(寫-寫 屏障)。

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

          原理圖12-volatile寫的場(chǎng)景如何插入內(nèi)存屏障
          ?

          StoreStore屏障可以保證在volatile寫(flag賦值操作flag=true)之前,其前面的所有普通寫(num的賦值操作num=1) 操作已經(jīng)對(duì)任意處理器可見了,保障所有普通寫在volatile寫之前刷新到主內(nèi)存。

          ?

          「volatile讀場(chǎng)景如何插入內(nèi)存屏障:」

          • 在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障(讀-讀 屏障)。

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

          原理圖13-volatile讀場(chǎng)景如何插入內(nèi)存屏障
          ?

          LoadStore屏障可以保證其后面的所有普通寫(num的賦值操作num=num+5) 操作必須在volatile讀(if(flag))之后執(zhí)行。

          ?

          十、volatile常見應(yīng)用

          這里舉一個(gè)應(yīng)用,雙重檢測(cè)鎖定的單例模式

          package?com.jackson0714.passjava.threads;
          /**
          ?演示volatile?單例模式應(yīng)用(雙邊檢測(cè))
          ?*?@author:?悟空聊架構(gòu)
          ?*?@create:?2020-08-17
          ?*/


          class?VolatileSingleton?{
          ????private?static?VolatileSingleton?instance?=?null;
          ????private?VolatileSingleton()?{
          ????????System.out.println(Thread.currentThread().getName()?+?"\t?我是構(gòu)造方法SingletonDemo");
          ????}
          ????public?static?VolatileSingleton?getInstance()?{
          ????????//?第一重檢測(cè)
          ????????if(instance?==?null)?{
          ????????????//?鎖定代碼塊
          ????????????synchronized?(VolatileSingleton.class)?{
          ????????????????//?第二重檢測(cè)
          ????????????????if(instance?==?null)?{
          ????????????????????//?實(shí)例化對(duì)象
          ????????????????????instance?=?new?VolatileSingleton();
          ????????????????}
          ????????????}
          ????????}
          ????????return?instance;
          ????}
          }

          代碼看起來(lái)沒有問題,但是 instance = new VolatileSingleton();其實(shí)可以看作三條偽代碼:

          memory?=?allocate();?//?1、分配對(duì)象內(nèi)存空間
          instance(memory);?//?2、初始化對(duì)象
          instance?=?memory;?//?3、設(shè)置instance指向剛剛分配的內(nèi)存地址,此時(shí)instance?!=?null

          步驟2 和 步驟3之間不存在 數(shù)據(jù)依賴關(guān)系,而且無(wú)論重排前 還是重排后,程序的執(zhí)行結(jié)果在單線程中并沒有改變,因此這種重排優(yōu)化是允許的。

          memory?=?allocate();?//?1、分配對(duì)象內(nèi)存空間
          instance?=?memory;?//?3、設(shè)置instance指向剛剛分配的內(nèi)存地址,此時(shí)instance?!=?null,但是對(duì)象還沒有初始化完成
          instance(memory);?//?2、初始化對(duì)象

          如果另外一個(gè)線程執(zhí)行:if(instance == null)時(shí),則返回剛剛分配的內(nèi)存地址,但是對(duì)象還沒有初始化完成,拿到的instance是個(gè)假的。如下圖所示:

          原理圖14-雙重檢鎖存在的并發(fā)問題

          解決方案:定義instance為volatile變量

          private?static?volatile?VolatileSingleton?instance?=?null;

          十一、volatile都不保證原子性,為啥我們還要用它?

          奇怪的是,volatile都不保證原子性,為啥我們還要用它?

          volatile是輕量級(jí)的同步機(jī)制,對(duì)性能的影響比synchronized小。

          ?

          典型的用法:檢查某個(gè)狀態(tài)標(biāo)記以判斷是否退出循環(huán)。

          ?

          比如線程試圖通過類似于數(shù)綿羊的傳統(tǒng)方法進(jìn)入休眠狀態(tài),為了使這個(gè)示例能正確執(zhí)行,asleep必須為volatile變量。否則,當(dāng)asleep被另一個(gè)線程修改時(shí),執(zhí)行判斷的線程卻發(fā)現(xiàn)不了。

          「那為什么我們不直接用synchorized,lock鎖?它們既可以保證可見性,又可以保證原子性為何不用呢?」

          因?yàn)閟ynchorized和lock是排他鎖(悲觀鎖),如果有多個(gè)線程需要訪問這個(gè)變量,將會(huì)發(fā)生競(jìng)爭(zhēng),只有一個(gè)線程可以訪問這個(gè)變量,其他線程被阻塞了,會(huì)影響程序的性能。

          ?

          注意:當(dāng)且僅當(dāng)滿足以下所有條件時(shí),才應(yīng)該用volatile變量

          • 對(duì)變量的寫入操作不依賴變量的當(dāng)前值,或者你能確保只有單個(gè)線程更新變量的值。
          • 該變量不會(huì)與其他的狀態(tài)一起納入不變性條件中。
          • 在訪問變量時(shí)不需要加鎖。
          ?

          十二、volatile和synchronzied的區(qū)別

          • volatile只能修飾實(shí)例變量和類變量,synchronized可以修飾方法和代碼塊。
          • volatile不保證原子性,而synchronized保證原子性
          • volatile 不會(huì)造成阻塞,而synchronized可能會(huì)造成阻塞
          • volatile 輕量級(jí)鎖,synchronized重量級(jí)鎖
          • volatile 和synchronized都保證了可見性和有序性

          十三、小結(jié)

          • volatile 保證了可見性:當(dāng)一個(gè)線程修改了共享變量的值時(shí),其他線程能夠立即得知這個(gè)修改。
          • volatile 保證了單線程下指令不重排:通過插入內(nèi)存屏障保證指令執(zhí)行順序。
          • volatitle不保證原子性,如a++這種自增操作是有并發(fā)風(fēng)險(xiǎn)的,比如扣減庫(kù)存、發(fā)放優(yōu)惠券的場(chǎng)景。
          • volatile 類型的64位的long型和double型變量,對(duì)該變量的讀/寫具有原子性。
          • volatile 可以用在雙重檢鎖的單例模式中,比synchronized性能更好。
          • volatile 可以用在檢查某個(gè)狀態(tài)標(biāo)記以判斷是否退出循環(huán)。

          參考資料:

          《深入理解Java虛擬機(jī)》
          《Java并發(fā)編程的藝術(shù)》
          《Java并發(fā)編程實(shí)戰(zhàn)》
          瀏覽 45
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  9久热 | 日韩无码日韩有码 | 美日韩一级黄色片中文字幕 | 毛片A片 毛片A区 | 欧美一级免费A片 |