淺談所謂的并發(fā)

淺談并發(fā)

前言:之所以寫這篇文章,是想自己總結(jié)一下對并發(fā)的理解,看看一篇文章能不能把并發(fā)說清楚!跟耗子大佬想法一樣,還是建議大家多去閱讀經(jīng)典的書籍,盡量少碎片化學(xué)習(xí),同時如果有出錯之處可以被大家指出。

想了很久,什么是并發(fā)的基礎(chǔ),基于Java來說,或者很多第一印象就是JUC包,CAS,但其實(shí)這些只是工具,各種的容器,CAS如果基于Intel也只是CMPXCHG的硬件原語,我其實(shí)更偏向于把這些稱之為工具-tool,就好像戰(zhàn)爭中的劍或者刃,如果使用者是一個絲毫不懂武道的文人,是絕對發(fā)揮不出刃的威力的。工具永遠(yuǎn)是工具,而并發(fā)編程是一種思想,不限定于語言。

因此,我認(rèn)為并發(fā)是基于OS的多道程序設(shè)計(jì),也就是先有多道程序設(shè)計(jì)才會有并發(fā)一說,在一開始計(jì)算機(jī)發(fā)展的單機(jī)單進(jìn)程計(jì)算機(jī)是不存在并發(fā)一說,并發(fā)是用來解決IO和CPU之間的不協(xié)調(diào)的,或者這樣說有點(diǎn)高大上,簡單來說就是充分壓榨CPU性能的一種思想。

多任務(wù)處理(英語:Computer multitasking)是指計(jì)算機(jī)同時運(yùn)行多個程序的能力。多任務(wù)的一般方法是運(yùn)行第一個程序的一段代碼,保存工作環(huán)境;再運(yùn)行第二個程序的一段代碼,保存環(huán)境;……恢復(fù)第一個程序的工作環(huán)境,執(zhí)行第一個程序的下一段代碼……現(xiàn)代的多任務(wù),每個程序的時間分配相對平均。 -- wikipedia

而硬件基礎(chǔ)就現(xiàn)代來說更多的是DMA、CPU、內(nèi)存,在這基礎(chǔ)就會誕生好幾種IO,BIO、NIO、AIO,基于硬件和一堆奇特的數(shù)據(jù)結(jié)構(gòu)以及算法,更充分地去提高資源利用率,把單機(jī)的利用效率提高,例如著名的C10K問題。

并發(fā)與并行

有很多人會把并發(fā)和并行混淆,把并發(fā)和并行的概念混在一起說,認(rèn)為并發(fā)=并行。——這種想法其實(shí)是錯誤的。

首先說一下并行:

  • 并行的基礎(chǔ)是多CPU(先不說目前超線程技術(shù)——一個CPU同時運(yùn)行多個線程)

    • 如果沒有多CPU,是不存在并行一說的,并行其實(shí)就是同一時間節(jié)點(diǎn)存在多個執(zhí)行流(執(zhí)行流其實(shí)就是通常所說的Thread,幾個CPU幾個執(zhí)行流)去執(zhí)行task任務(wù),執(zhí)行同一部分的代碼塊。

    • 換言之,可以理解為在某一時間節(jié)點(diǎn)(例如今天2.),多個人去做職責(zé)相同的事情(類似或者相同的事情,例如幾個人都在一起玩一樣的游戲)。

  • 并發(fā)的基礎(chǔ)是多道程序設(shè)計(jì)

    • 首先明確一點(diǎn),CPU資源是很寶貴的,而并發(fā)的原理就是當(dāng)CPU需要等待某些資源的時候(IO,設(shè)備傳輸、Linux來說一切皆文件,外接的設(shè)備分為流設(shè)備、塊設(shè)備,流設(shè)備著名代表就是網(wǎng)絡(luò)傳輸,塊設(shè)備更多的是硬盤),智能的調(diào)度程序會先讓其他需要CPU資源去運(yùn)算的線程(執(zhí)行流)先霸占CPU進(jìn)行運(yùn)算,避免因等待外部資源而浪費(fèi)CPU資源,造成效率下降。

    • 那多道程序設(shè)計(jì)又是啥?多道程序設(shè)計(jì)在操作系統(tǒng)的體現(xiàn)其實(shí)就是調(diào)度程序,如Linux的調(diào)度程序把CPU資源抽象成一個個的時間片,每個執(zhí)行流只占有一部分時間片,擁有當(dāng)前時間片就是當(dāng)前使用CPU的執(zhí)行流。那么,擁有現(xiàn)在或者未來一段時間時間片的線程(執(zhí)行流)宏觀上來說狀態(tài)都是TASK_RUNNING(Linux線程的狀態(tài)之一),也就是它都是運(yùn)行著的(只要底層切換以及運(yùn)算夠快,上層就是無感知的),這也是在單核CPU的機(jī)器上,同時運(yùn)行多個程序的奧秘,你只是被CPU給欺騙了。只要我運(yùn)行得夠快,你就不知道我有沒有切換。說句題外話,這也是硬件+程序配合的結(jié)果,使用足夠多的寄存器空間去輔助保證切換效率,反觀也是空間和時間這對矛盾在計(jì)算機(jī)體系的體現(xiàn)。

并發(fā)問題的根源

既然并發(fā)程序可以大大提高資源利用率,這里請大家記住一句話,Every coin has two sides,沒有十全十美的解決方案。

并發(fā)程序同時會帶來很多問題,最常見的就是運(yùn)行效果與預(yù)想不一致,死鎖等。

那么為什么會出現(xiàn)這樣的問題?下面將從幾方面去看一看,變量的可見性、代碼的原子性、代碼的有序性(禁止重排序)。

處理器緩存

嚴(yán)格來說,這個問題只會出現(xiàn)在多CPU的場景下。

但是目前相信家用機(jī)或者服務(wù)器基本不會只配備單CPU,即使你跟我說阿里云我還是能租到到單CPU的機(jī)器,不知道你有沒有認(rèn)真留意過,基本你能租到的機(jī)器一般叫VM(虛擬機(jī)),物理機(jī)器租賃幾乎越來越少了,VM宿主們還是多CPU的機(jī)器,只不過調(diào)度程序粒度更大而已,數(shù)據(jù)中心本身就是一個粒度更大的操作系統(tǒng)(可以參考阿里的飛天系統(tǒng)概念)。同時,你租到的還有目前大熱的容器(如著名的docker),其實(shí)只是虛擬化的程度(或者方式)不一樣而已,底層還是多臺多CPU的機(jī)器,不過在基礎(chǔ)上進(jìn)行了抽象以及隔離。

現(xiàn)代計(jì)算機(jī)體系結(jié)構(gòu)下,主存(也就是你所說的內(nèi)存條)是不會直接與CPU通信的,中間必然隔著多層緩存(L1、L2甚至L3等),為什么要如此設(shè)置?詳見下面

image.png
image-20200616165256878

先說CPU的速度,拿2.6Ghz來說,每秒執(zhí)行2.6*10^9個指令,也就是說一個指令需要0.38ns,而主存需要100ns,大概是263倍,也就是CPU那里過一天,主存大概能過大半年了,兩者直接通信,受短板效應(yīng)影響CPU效率大大降低,作為CPU這你受得了嗎?于是乎就有了中間的L1、L2等多級緩存作為緩沖區(qū),作為通信的過渡。

每個CPU都有自己的緩存,意味著當(dāng)多個CPU執(zhí)行同一部分代碼的時候,一個變量在多個CPU下就有多個副本了,同時CPU緩存對于不同CPU來說是不可見的,于是乎變量之間就會產(chǎn)生不一致性問題。

線程切換

上文說到其實(shí)目前大部分操作系統(tǒng)調(diào)度算法基于分時技術(shù),多個進(jìn)程(實(shí)際上每個進(jìn)程至少有一個或多個thread執(zhí)行流,linux中線程等同于lwp輕量級進(jìn)程,可以將此處進(jìn)程理解為日常所謂的線程,下面不再說明)以“時間多路復(fù)用”方式運(yùn)行。CPU時間被分成“片(slice)”,單處理器在任一時刻只能運(yùn)行一個進(jìn)程,如果當(dāng)前運(yùn)行進(jìn)程的時間片或時限(quantum)到期,該進(jìn)程還沒有運(yùn)行完畢,進(jìn)程切換就可以發(fā)生,分時依賴于定時中斷,因此對進(jìn)程透明。 ——《深入理解Linux內(nèi)核》

其實(shí)上面就意味著多個線程的代碼其實(shí)需要分配時間片交替執(zhí)行,當(dāng)多個線程運(yùn)行不同的代碼確實(shí)不存在問題,但是假如一段需要原子執(zhí)行的代碼被并發(fā)執(zhí)行,原子代碼還未執(zhí)行完成發(fā)生了線程切換,這時候運(yùn)行的結(jié)果就不一定跟預(yù)想一致。

上文例子可以參考,JDK中的HashMap中進(jìn)行resize擴(kuò)容的元素遷移中的循環(huán)遷移行為造成環(huán)形鏈表,假如兩個線程同時進(jìn)行resize,在執(zhí)行語句next = e.next發(fā)生線程切換,造成變量錯亂從而形成環(huán)形鏈表,具體請查閱資料。

編譯重排序、CPU指令并行、亂序執(zhí)行(內(nèi)存操作順序)

當(dāng)編譯器編譯或者CPU執(zhí)行代碼時候,在前后沒有“數(shù)據(jù)依賴”情況下“聰明”的編譯器或者CPU都會對代碼順序進(jìn)行調(diào)整,以提高性能,但調(diào)整后代碼執(zhí)行結(jié)果需要按照as-if-serail原則,也就是跟單線程執(zhí)行結(jié)果一致。

亂序執(zhí)行的作用?為什么要亂序執(zhí)行?

——進(jìn)行重排序以更好地利用寄存器或者緩存提高性能

假設(shè)指令A(yù)BC,AC使用了寄存器X,B沒有使用寄存器X,同時三指令之間沒有數(shù)據(jù)依賴,那么編譯器或者CPU就會先執(zhí)行C以免去寄存器的重復(fù)裝配。

可是多線程執(zhí)行結(jié)果呢?這個是不被保證的,下面可以看一段雙重檢查創(chuàng)建單例代碼

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n53" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;">public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}</pre>

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n73" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;">public class Singleton {
static volatile Singleton instance;//也就是此處
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}</pre>

并發(fā)編程網(wǎng)

geek的《并發(fā)編程實(shí)戰(zhàn)》

《Java并發(fā)編程技術(shù)》

最后,列一下參考出處,但可能存在遺漏:

如何理解channel是第一類對象?并發(fā)編程時候從channel出發(fā),多利用channel去協(xié)調(diào)執(zhí)行流的執(zhí)行順序,而不是像Java一樣二話不說直接加個鎖,讓執(zhí)行流們搶個頭破血流的。

相對于Actor模型,CSP中channel是第一類對象,它不關(guān)注發(fā)送消息的實(shí)體,而關(guān)注與發(fā)送消息時使用的channel。這就是所謂顯式通信。

不要通過共享內(nèi)存來通信,而要通過通信來實(shí)現(xiàn)內(nèi)存共享。

Do not communicate by sharing memory; instead, share memory by communicating.

不知道大家有沒有看過這句話:

Channel——管道,跟OS中的管道有點(diǎn)類似,Go中的Channel其實(shí)出自CSP通信模型中的Channel。

此處不打算對GoRoutine深入解析,因?yàn)榕c本文內(nèi)容無關(guān),而GoRoutine原理也只是在Application用戶態(tài)利用協(xié)程的概念自己實(shí)現(xiàn)調(diào)度,減少上下文切換,提升運(yùn)行速度,同時加WorkStealing平衡多Core效率。不知道大家有沒有發(fā)現(xiàn),當(dāng)?shù)讓訌?fù)雜的邏輯滿足不了需求時候,或許換種思維方式,把復(fù)雜邏輯抽離到上層,保持底層簡單邏輯,只要上層實(shí)現(xiàn)更輕量,效率就會大大提升。這同時也是QUIC的設(shè)計(jì)美學(xué),也是Google常用的套路。

眾所周知,Go的兩大語言核心——GoRoutine和Channel

Go的顯式通信解決方案

  • 有人說ReentrantLock的性能比監(jiān)視器鎖好,其實(shí)JDK1.6后引入了偏向鎖、輕量級鎖后性能基本持平

  • ReentrantLock多了一個Condition的概念,其實(shí)就是上文所說的條件變量。而Synchronized是不存在這個概念的。

說到這里,還是有必要科普下Java中監(jiān)視器鎖和DungLea大牛JUC包中的AQS設(shè)計(jì)出來的鎖(例如ReentrantLock)的區(qū)別:

image-20200618110438744

管程還引入了條件變量的概念,每個條件變量都有一個等待隊(duì)列,當(dāng)條件變量不為真,線程先進(jìn)入條件變量的等待隊(duì)列,而不是總的入口等待隊(duì)列

如何解決同步?管程模型中,共享變量和對共享變量的操作是被封裝起來的,圖中最外層的框就代表封裝的意思。框的上面只有一個入口,并且在入口旁邊還有一個入口等待隊(duì)列。當(dāng)多個線程同時試圖進(jìn)入管程內(nèi)部時,只允許一個線程進(jìn)入,其他線程則在入口等待隊(duì)列中等待。

image-20200618110041555

如何解決互斥?現(xiàn)在來看一個隊(duì)列queue的例子,支持入隊(duì)enq()和出隊(duì)deq(),如果線程A和線程B想訪問共享變量queue,只能通過封裝好的而且管程提供具有互斥的enq()和deq()操作。

管程解決并發(fā)問題核心就是:管理共享變量以及對共享變量的操作過程,讓他們支持并發(fā)

管程的歷史上出現(xiàn)三種模型,Hasen、Hoare和MESA模型,而現(xiàn)在廣泛應(yīng)用的是MESA模型,Java也是采用的MESA模型。

管程 —— Java的隱式通信解決放哪

最后,介紹一個通用的并發(fā)編程模型,相信也是相對更著名或者最廣為人知的,它是屬于隱式通信、顯式同步的一種。

  1. 顯式通信,隱式同步 —— 以消息傳遞先溝通好順序以達(dá)到同步的效果

  2. 隱式通信,顯式同步 —— 爭搶以獲取順序達(dá)到同步,其實(shí)是隱式的通信

上面所有的通信方式其實(shí)可以分為兩類

  • 鎖機(jī)制

    • 互斥鎖

    • 讀寫鎖

    • 條件變量

  • 信號signal

  • 信號量semaphore

線程通信:

  • 管道(也就是命令常用的|),命名管道

  • 消息隊(duì)列

  • 共享內(nèi)存

  • 信號

  • 信號量

  • 套接字Socket(不同機(jī)器進(jìn)程通信)

進(jìn)程通信:

先回顧一下操作系統(tǒng)的知識

為什么這樣說呢?這里可以講個大白話,當(dāng)你家門口只夠一個人通過,但是想進(jìn)入的人卻有好幾個,幾個人如果不進(jìn)行溝通(通信),一直堵在門口,是不是大家都進(jìn)不去?

其實(shí)兩大核心問題也就是一個問題,歸根到底就是進(jìn)程、線程的通信、協(xié)調(diào)(同步)問題

  • 互斥,同一時刻保證互斥資源的單一準(zhǔn)入性

  • 同步,線程如何通信、同步

并發(fā)領(lǐng)域其實(shí)有兩大核心問題:

上層如何處理

此處不對操作系統(tǒng)內(nèi)核中的內(nèi)存屏障深入解析,如果有必要請移步到linux內(nèi)核中內(nèi)存屏障

  1. 禁止編譯器以及CPU對設(shè)定內(nèi)存屏障對共享變量操作指令進(jìn)行重排序

  2. 保證緩存的可見性(把緩存中變量強(qiáng)行flush回主存),同時緩存一致性協(xié)議MESI(intel)會保證數(shù)據(jù)最終一致

首先說一下內(nèi)存屏障存在的作用:

內(nèi)存屏障

  1. 語言層面——循環(huán)+CAS操作

    • 循環(huán)+CAS操作(基于硬件原語CMPXCHG,下面會講到)去保證執(zhí)行具有原子性,下面給出一段Java代碼進(jìn)行計(jì)數(shù)器實(shí)現(xiàn)

    • 這里需要特別說明一下,循環(huán)+CAS操作是語言層面保證原子性的手段,但不限于語言(多說一句,很多人太拘泥于語言了,語言只是思想的表現(xiàn)形式,舉個例子,你敢說Vue的尤大寫Java會比你差)

      <pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n85" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;">private AtomicInteger atomicI = new AtomicInteger(0);
      private void safeCount() {
      for(;;) {
      int i = atomicI.get();
      boolean sunc = atomicI.compareAndSet(i, ++i);
      if(sunc) {
      break;
      }
      }
      }</pre>

    • 為什么這里沒有把各種鎖進(jìn)行解析,而是單獨(dú)拉出循環(huán)+CAS操作來進(jìn)行討論?

      • 其實(shí)很多語言的鎖機(jī)制實(shí)現(xiàn)的更粗粒度的原子操作的基礎(chǔ)就是循環(huán)+CAS。

      • 鎖,一般解釋為把互斥資源保護(hù)起來,開放唯一的入口,通過額外的一個事物對象(鎖)去保證入口的單一準(zhǔn)入性?

      • 那么就會誕生一個問題——如何去保證入口的單一準(zhǔn)入性,那就是同一時刻保證只能有一個線程霸占互斥的資源?——循環(huán)+CAS

      • Java的synchronized原語中的偏向鎖、輕量級鎖、重量級鎖都是利用循環(huán)+CAS去獲取鎖(爭奪monitor對象,CAS更換對象頭的markword)

  2. 硬件層面

    看到這里不知道你有沒有刨根問底的精神,那么CAS的底層總得要保證CAS硬件原語的原子性操作吧,上層可以保證原子性,底層如果不能保證原子性那上層的功夫豈不是白搭?

    CPU保證從系統(tǒng)內(nèi)存中讀取或者寫入一個字節(jié)是原子的,意思是當(dāng)一個處理讀取一個字節(jié)時候,其他處理不能訪問這個字節(jié)的訪問地址,這就是CPU自動保證基本的內(nèi)存操作原子性。而Pentium6和最新的CPU能自動保證單處理器對同一緩存行進(jìn)行16/32/64位的操作是原子的。

    而這里主要講述多CPU之間更復(fù)雜的內(nèi)存原子操作(跨多個緩存行、跨總線寬度、跨頁表),例如i++,CPU其實(shí)提供了兩種解決方案:

    • 鎖定總線

      • CPU是多個的,但是主存只有一個,一對多的關(guān)系,之間通信肯定需要一個東西去鏈接。這個東西就是通常所說的——總線,總線會去協(xié)調(diào)CPU和主存的通信,不能在同一時刻多個CPU存取同一個數(shù)據(jù)(一個字節(jié)),這就是總線的仲裁,把一些必要的并行操作給串行(實(shí)際上設(shè)計(jì)復(fù)雜,為了提升效率,還有會有兩個著名的架構(gòu)SMP和NUMA,這里暫時不討論NUMA,NUMA其實(shí)就是類似數(shù)據(jù)庫為了提高性能進(jìn)行分庫分表嘛),這就是上面說的CPU自動保證基本的內(nèi)存操作原子性。

      • 那么既然總線負(fù)責(zé)多CPU與主存通信,那么我們把總線給鎖掉是不是就能保證讀改寫共享變量的原子性了。這就是所謂的總線鎖。使用處理器提供的一個LOCK#信號,當(dāng)一個處理器在總線上輸出此信號時,其他處理器請求將被阻塞,處理器可以獨(dú)占共享內(nèi)存。

    • 鎖定緩存

      鎖定總線意味著所有CPU在鎖定時間內(nèi)不能對內(nèi)存進(jìn)行讀寫操作,那么性能開銷是不是有點(diǎn)大?那么我們可不可以換個思路,把鎖的粒度變小(其實(shí)ConcurrentHashMap的鎖也由一開始的Segment段鎖到現(xiàn)在的桶鎖,減小鎖的粒度,提高效率)。

      • 頻繁使用的內(nèi)存會緩存在處理器L1、L2、L3等高速緩存上,其實(shí)原子操作可以直接在處理器內(nèi)部緩存進(jìn)行,不需要總線那么重粒度那么粗的鎖。在Pentium6和現(xiàn)在的處理器可以使用“緩存鎖定”保證原子性。

      • “緩存鎖定”其實(shí)是指內(nèi)存區(qū)域如果被緩存在cpu緩存行,CPU執(zhí)行鎖回寫主存時候,不聲言LOCK#總線鎖,而是修改內(nèi)部的內(nèi)存地址,而本身CPU的緩存一致性機(jī)制(CPU緩存會嗅探系統(tǒng)內(nèi)存和其他CPU緩存,保持?jǐn)?shù)據(jù)一致,如果嗅探到有CPU寫內(nèi)存地址,會使緩存行無效)會阻止同時修改由兩個以上CPU緩存的內(nèi)存區(qū)域數(shù)據(jù),當(dāng)其他處理回寫已被鎖定的緩存行數(shù)據(jù),會使緩存行無效。

下面將從兩方面去講述原子性的保證,這里的原子性希望大家與上層代碼的原子性分開,這里更多地是保證基本指令操作的原子性,也就是底層硬件對原子性的擔(dān)保,而上層的各種鎖應(yīng)用(基于管程)也是得以下層硬件原子性保障才可以實(shí)現(xiàn)原子性的操作。因此,注意區(qū)分兩個原子性,一個是偏向于代碼業(yè)務(wù)邏輯事務(wù)執(zhí)行的原子性,例如數(shù)據(jù)庫的事務(wù)提交,一個是底層指令執(zhí)行的保障以及語言層面鎖的設(shè)計(jì)基礎(chǔ)。

保證操作原子性

底層如何去保障

所以一般都會給instance 加上volatile修飾,volatile在開始老版本JDK作用僅僅是處理了處理器緩存,后來發(fā)現(xiàn)volatile功能太輕了,在新規(guī)范JSR-133給加上禁止重排序語義。

單線程執(zhí)行問題也不大,但是多線程呢?假設(shè)線程A完成2操作后發(fā)生線程切換,這時候線程B來了,判斷第一個條件instance != null,直接返回空的對象,這樣問題就大了去了。

  1. 分配一塊內(nèi)存 M;

  2. 將 M 的地址賦值給 instance 變量;

  3. 最后在內(nèi)存 M 上初始化 Singleton 對象。

但實(shí)際優(yōu)化后執(zhí)行順序是:

  1. 分配內(nèi)存M

  2. 在內(nèi)存中實(shí)例化對象Singleton

  3. 把M的地址賦值給變量instance

我們再來看一下new操作,jvm規(guī)范中:

假設(shè)兩個線程同時去取Singleton的實(shí)例并發(fā)現(xiàn)instance為null所以進(jìn)行實(shí)例創(chuàng)建,兩個線程會對鎖進(jìn)行競爭,只有一個能獲得鎖,假設(shè)線程A拿到了想要的鎖,開始創(chuàng)建實(shí)例,然后進(jìn)行解鎖操作,線程B接著取得鎖發(fā)現(xiàn)instance != null(第二處),最后兩個線程都取到單一的實(shí)例對象。看上去很完美。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容