備注:本文是《Java并發(fā)編程的藝術(shù)》一書(shū)的讀書(shū)筆記,。 Java并發(fā)機(jī)制的底層原理實(shí)現(xiàn)volatile的應(yīng)用 volatile是輕量級(jí)的synchronized,他在多處理器開(kāi)發(fā)中保證了共享變量的“可見(jiàn)性”,。可見(jiàn)性的意思是當(dāng)一個(gè)線程修改一個(gè)共享變量時(shí),,另外一個(gè)線程能讀到這個(gè)修改的值,。如果volatile變量修飾符使用恰當(dāng)?shù)脑挘萻ynchronized的使用和執(zhí)行成本更低,,因?yàn)樗粫?huì)引起線程上下文的切換和調(diào)度,。
synchronized的實(shí)現(xiàn)原理與應(yīng)用 synchronized實(shí)現(xiàn)同步的基礎(chǔ):Java中的每一個(gè)對(duì)象都可以作為鎖。具體表現(xiàn)為以下3中形勢(shì)* 1.對(duì)于普通同步方法,,鎖是當(dāng)前實(shí)例對(duì)象,。 2.對(duì)于靜態(tài)同步方法,鎖是當(dāng)前類(lèi)的Class對(duì)象,。 3.對(duì)于同步方法塊,,鎖是Synchronized括號(hào)里配置的對(duì)象。 當(dāng)一個(gè)線程試圖訪問(wèn)同步代碼塊時(shí),,它首先必須得到鎖,,推出或拋出異常時(shí)必須釋放鎖。 偏向鎖 當(dāng)一個(gè)線程訪問(wèn)同步塊并獲取鎖時(shí),,會(huì)在對(duì)象頭和棧幀中的鎖記錄里存儲(chǔ)鎖偏向的線程ID,,以后該線程在進(jìn)入和退出同步塊時(shí)不需要進(jìn)行CAS操作來(lái)加鎖和解鎖,只需簡(jiǎn)單地測(cè)試一下對(duì)象頭的Mark Word里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖,。如果測(cè)試成功,,表示線程已經(jīng)獲得了鎖。如果測(cè)試失敗,,則需要再測(cè)試一下Mark Word中偏向鎖的標(biāo)識(shí)是否設(shè)置成1(表示當(dāng)前是偏向鎖):如果沒(méi)有設(shè)置,,則使用CAS競(jìng)爭(zhēng)鎖;如果設(shè)置了,則嘗試使用CAS將對(duì)象頭的偏向鎖指向當(dāng)前線程,。偏向鎖使用了一種等到競(jìng)爭(zhēng)出現(xiàn)才釋放鎖的機(jī)制,,所以當(dāng)其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖,。偏向鎖的撤銷(xiāo),,需要等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒(méi)有正在執(zhí)行的字節(jié)碼)。 輕量級(jí)鎖 線程在執(zhí)行同步塊之前,,JVM會(huì)先在當(dāng)前線程的棧幀中創(chuàng)建用于存儲(chǔ)鎖記錄的空間,,并將對(duì)象頭中的Mark Word復(fù)制到鎖記錄中,,官方稱(chēng)為Displaced Mark Word。然后線程嘗試CAS將對(duì)象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,,當(dāng)前線程獲得鎖,,如果失敗,,表示其他線程競(jìng)爭(zhēng)鎖,,當(dāng)前線程便嘗試使用自旋來(lái)獲取鎖。輕量級(jí)解鎖時(shí),,會(huì)使用原子的CAS操作將Displaced Mark Word 替換回到對(duì)象頭,,如果成功,則表示沒(méi)有競(jìng)爭(zhēng)發(fā)生,。如果失敗,,表示當(dāng)前鎖存在競(jìng)爭(zhēng),鎖就會(huì)膨脹成重量級(jí)鎖,。因?yàn)樽孕龝?huì)消耗CPU,,為了避免無(wú)用的自旋(比如獲得鎖的線程被阻塞住了),,一旦確認(rèn)升級(jí)成重量級(jí)鎖,,就不會(huì)再恢復(fù)到輕量級(jí)鎖狀態(tài)。當(dāng)鎖處于這個(gè)狀態(tài)下,,其他線程試圖獲取鎖時(shí),,都會(huì)被阻塞住,當(dāng)持有鎖的線程釋放鎖之后會(huì)喚醒這些線程,,被喚醒的線程就會(huì)進(jìn)行新一輪的奪鎖之爭(zhēng),。 鎖的優(yōu)缺點(diǎn)對(duì)比 鎖 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場(chǎng)景 | 偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法相比僅存在納秒級(jí)的差距 | 如果線程間存在鎖競(jìng)爭(zhēng),,會(huì)帶來(lái)額外的鎖撤銷(xiāo)的消耗 | 適用于只有一個(gè)線程訪問(wèn)同步塊場(chǎng)景 | 輕量級(jí)鎖 | 競(jìng)爭(zhēng)的線程不會(huì)阻塞,,提高了線程的響應(yīng)速度 | 如果始終得不到鎖競(jìng)爭(zhēng)的線程,使用自旋會(huì)消耗CPU | 追求響應(yīng)時(shí)間 同步塊執(zhí)行速度非???/td> | 重量級(jí)鎖 | 線程競(jìng)爭(zhēng)不適用自旋,,不會(huì)消耗CPU | 線程阻塞,響應(yīng)時(shí)間緩慢 | 追求吞吐量 同步塊執(zhí)行速度較長(zhǎng) |
原子操作的實(shí)現(xiàn)原理 原子(atomic)本意是“不能被進(jìn)一步分割的最小粒子“,,而原子操作(atomic operation)意為“不可被中斷的一個(gè)或一系列操作“,。 術(shù)語(yǔ)定義 術(shù)語(yǔ)名稱(chēng) | 英文 | 解釋 | 緩存行 | Cache line | 緩存的最小操作單位 | 比較并交換 | Compare and Swap | CAS操作需要輸入兩個(gè)數(shù)值,,一個(gè)舊值(期望操作前的值)和一個(gè)新值,在操作期間先比較舊值有沒(méi)有發(fā)生變化,,如果沒(méi)有發(fā)生變化,,才交換成新值,發(fā)生了變化則不交換 | CPU流水線 | CPU pipeline | CPU流水線的工作方式就像工業(yè)生產(chǎn)上的裝配流水線,,在CPU中由5-6個(gè)不同功能的電路單元組成一條指令處理流水線,,然后將一條X86指令分成5-6步后再由這點(diǎn)電路單元分別執(zhí)行,這樣就能實(shí)現(xiàn)在一個(gè)CPU時(shí)鐘周期完成一條指令,,因此提高CPU的運(yùn)算速度 | 內(nèi)存順序沖突 | Memory order violation | 內(nèi)存順序沖突一般是由假共享引起的,,假共享是指多個(gè)CPU同時(shí)修改同一個(gè)緩存行的不同部分而引起其中一個(gè)CPU的操作無(wú)效,當(dāng)出現(xiàn)這個(gè)內(nèi)存順序沖突時(shí),,CPU必須清空流水線 |
處理器如何實(shí)現(xiàn)原子操作 處理器提供總線鎖定和緩存鎖定兩個(gè)機(jī)制來(lái)保證復(fù)雜內(nèi)存操作的原子性 (1)使用總線鎖保證原子性:第一機(jī)制是通過(guò)總線鎖保證原子性,。保證CPU1讀改寫(xiě)共享變量的時(shí)候,CPU2不能操作緩存了該共享變量?jī)?nèi)存地址的緩存,。所謂總線鎖就是使用處理器提供的一個(gè)LOCK#信號(hào),,當(dāng)一個(gè)處理器在總線上輸出此信號(hào)時(shí),其他處理器的請(qǐng)求將被阻塞住,,那么該處理器可以獨(dú)享共享內(nèi)存,。 (2)使用緩存鎖保證原子性:第二個(gè)機(jī)制是通過(guò)緩存鎖定來(lái)保證原子性。在同一時(shí)刻,,我們只需保證對(duì)某個(gè)內(nèi)存地址的操作是原子性即可,,但總線鎖定把CPU和內(nèi)存之間的通信鎖住了,這使得鎖定期間,,其他處理器不能操作其它內(nèi)存地址的數(shù)據(jù),,所以總線鎖定的開(kāi)銷(xiāo)比較大,目前處理器在某些場(chǎng)景下使用緩存鎖定代替總線鎖定來(lái)進(jìn)行優(yōu)化,。所謂“緩存鎖定”是指內(nèi)存區(qū)域如果被緩存在處理器的緩存行中,,并且在Lock操作期間被鎖定,那么當(dāng)其他執(zhí)行鎖操作回寫(xiě)到內(nèi)存時(shí),,處理器不在總線上聲言LOCK#信號(hào),,而是修改內(nèi)部的內(nèi)存地址,并允許它的緩存一致性機(jī)制來(lái)保證操作的原子性,,因?yàn)榫彺嬉恢滦詸C(jī)制會(huì)阻止同時(shí)修改由兩個(gè)以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù),,到其他處理器回寫(xiě)已被鎖定的緩存行數(shù)據(jù)時(shí),會(huì)使緩存行無(wú)效,。 但是有兩種情況下處理器不回使用緩存鎖定:當(dāng)操作的數(shù)據(jù)不能被緩存在處理器內(nèi)部,,或操作的數(shù)據(jù)跨多個(gè)緩存行時(shí),則處理器會(huì)調(diào)用總線鎖定。當(dāng)有些處理器不支持緩存鎖定,。
(1)使用循環(huán)CAS實(shí)現(xiàn)原子操作:JVM中的CAS操作正是利用了處理器提供的CMPXCHG指令實(shí)現(xiàn)的,。自旋CAS實(shí)現(xiàn)的基本思路就是循環(huán)進(jìn)行CAS操作直到成功為止。從Java1.5開(kāi)始,,JDK的并發(fā)包里提供了一些類(lèi)來(lái)支持原子操作,,如AtomicBoolean。 (2)CAS實(shí)現(xiàn)原子操作的三大問(wèn)題:ABA問(wèn)題,,因?yàn)镃AS需要在操作值的時(shí)候,,檢查值有沒(méi)有發(fā)生變化,如果沒(méi)有發(fā)生變化則更新,,但是如果一個(gè)值原來(lái)是A,,變成了B,又變成了A,,那么使用CAS進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)它的值沒(méi)有變化,,但是實(shí)際上卻變化了。ABA問(wèn)題的解決思路是使用版本號(hào),,在變量前追加版本號(hào),,每次變量更新的時(shí)候把版本號(hào)加1,那么A-B-A就會(huì)變成了1A-2B-3A,。從JAVA1.5開(kāi)始,,JDK的Atomic包里提供了一個(gè)類(lèi)AtomicStampedreference來(lái)解決ABA問(wèn)題。這個(gè)類(lèi)的compareAndSet方法的作用是首先檢查當(dāng)前引用是否等于預(yù)期引用,,并且檢查當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志,,如果全部相等,則以原子方式將該引用和該標(biāo)識(shí)的值設(shè)置為給定的更新值,。循環(huán)時(shí)間長(zhǎng)開(kāi)銷(xiāo)大,,自旋CAS如果長(zhǎng)時(shí)間不成功,會(huì)給CPU帶來(lái)非常大的執(zhí)行開(kāi)銷(xiāo),。如果JVM能支持處理器提供的pause指令,,那么效率會(huì)有一定的提升,。pause指令有兩個(gè)作用:第一,,它可以延遲流水線執(zhí)行指令,使CPU不會(huì)消耗過(guò)多的執(zhí)行資源,,延遲的時(shí)間取決于具體的版本,,在一些處理器上延遲時(shí)間是零;第二,,它可以避免在退出循環(huán)的時(shí)候因內(nèi)存順序沖突而引起CPU流水線被清空,,從而提高CPU的執(zhí)行效率。只能保證一個(gè)共享變量的原子性,當(dāng)對(duì)一個(gè)共享變量執(zhí)行操作時(shí),,我們可以使用循環(huán)CAS的方式來(lái)保證原子操作,,但是對(duì)多個(gè)共享變量操作時(shí),循環(huán)CAS就無(wú)法保證操作的原子性,,這個(gè)時(shí)候就可以用鎖,。還有一個(gè)取巧的辦法,就是把多個(gè)共享變量合并成一個(gè)共享變量來(lái)操作,。Java1.5以后JDK提供了AtomicReference類(lèi)來(lái)保證引用對(duì)象之間的原子性,,就可以把多個(gè)變量放在一個(gè)對(duì)象里來(lái)進(jìn)行CAS操作。 (3)使用鎖機(jī)制實(shí)現(xiàn)原子操作:鎖機(jī)制保證了只有獲得鎖的線程才能狗操作鎖定的內(nèi)存區(qū)域,。JVM內(nèi)部實(shí)現(xiàn)了很多種鎖機(jī)制,,有偏向鎖、輕量級(jí)鎖和互斥鎖,。有意思的是除了偏向鎖,,JVM實(shí)現(xiàn)鎖的方式都用了循環(huán)CAS,即一個(gè)線程想進(jìn)入同步塊的時(shí)候使用循環(huán)CAS的方式來(lái)獲取鎖,,當(dāng)它退出同步塊的時(shí)候使用循環(huán)CAS釋放鎖,。 Java內(nèi)存模型的基礎(chǔ) 在并發(fā)編程中,需要處理兩個(gè)關(guān)鍵問(wèn)題:線程之間如何通信及線程之間如何同步(這里的線程是指并發(fā)執(zhí)行的活動(dòng)實(shí)體),。通行是指線程之間以何種機(jī)制來(lái)交換信息,。在命令式編程中,線程之間的通信機(jī)制有兩種:內(nèi)存共享和消息傳遞,。 同步 是指程序中用于控制不同線程間操作發(fā)生相對(duì)順序的機(jī)制,。在共享內(nèi)存并發(fā)模型里,同步是顯性進(jìn)行的,。程序員需要顯性執(zhí)行某個(gè)方法或某段代碼需要在線程之間互斥執(zhí)行,。在消息傳遞的并發(fā)模型里,由于消息的發(fā)送必須在消息的接受之前,,因此同步是隱形進(jìn)行的,。 Java并發(fā)采用的是共享內(nèi)存模型。 JMM通過(guò)控制主內(nèi)存與每個(gè)線程的本地內(nèi)存之間的交互,,來(lái)為Java程序員提供內(nèi)存可見(jiàn)性,。 從源代碼到指令序列的重排序在執(zhí)行程序時(shí),為了提高性能,,編譯器和處理器常常會(huì)對(duì)指令做重排序,,重排序分3種類(lèi)型。 1)編譯器優(yōu)化的重排序,。編譯器在不改變單線程程序語(yǔ)義的前提下,,可以重新安排語(yǔ)句的執(zhí)行順序,。 2)指令級(jí)并行的重排序。現(xiàn)代處理器采用了指令級(jí)并行技術(shù)來(lái)將多條指令重疊執(zhí)行,。如果不存在數(shù)據(jù)依賴(lài)性,,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。 3)內(nèi)存系統(tǒng)的重排序,。由于處理器使用緩存和讀/寫(xiě)緩沖區(qū),,這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。 happens-before簡(jiǎn)介 JSR-133(JDK1.5開(kāi)始Java使用的新的內(nèi)存模型)使用 happens-before 的概念來(lái)闡述操作之間的內(nèi)存可見(jiàn)性,。在JMM中,,如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見(jiàn),那么這兩個(gè)操作之間必須要存在happens-before關(guān)系,。這里提到的兩個(gè)操作既可以是在一個(gè)線程內(nèi),,也可以是在不同線程之間。 程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,,happens-before于該線程中的任意后續(xù)操作,。 監(jiān)視器鎖規(guī)則:對(duì)一個(gè)鎖的解鎖,happens-before于隨后對(duì)這個(gè)鎖的加鎖,。 volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫(xiě),,happens-before于任意后續(xù)對(duì)這個(gè) volatile域的讀。 傳遞性:如果A heppens-before于B,,且B happnes-before C,, 那么A happens-before C。 兩個(gè)操作之間具有happens-before關(guān)系,,并不意味著前一個(gè)操作必須要在后一個(gè)操作之前執(zhí)行,,happens-before僅僅要求前一個(gè)操作(執(zhí)行的結(jié)果)對(duì)后一個(gè)操作可見(jiàn),且前一個(gè)操作按順序排在第二個(gè)操作之前,。 重排序 重排序是指編譯器和處理器為了優(yōu)化程序性能而對(duì)指令序列進(jìn)行重新排序的一種手段,。 as-if-serial語(yǔ)義 as-if-serial的語(yǔ)義是:不管怎么重排序,單線程程序執(zhí)行的結(jié)果不能被改變,。編譯器,、runtime和處理器都必須遵守as-if-serial語(yǔ)義。 volatile的內(nèi)存語(yǔ)義可見(jiàn)性,。對(duì)一個(gè)volatile變量的讀,,總是能看到任意線程對(duì)這個(gè)volatile變量最后的寫(xiě)入。
原子性,。對(duì)任意單個(gè)volatile變量的讀/寫(xiě)具有原子性,,但類(lèi)似于volatile++這種復(fù)合操作不具有原子性。
volatile寫(xiě)-讀的內(nèi)存語(yǔ)義:當(dāng)寫(xiě)一個(gè)volatile變量時(shí),,JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存,。 volatile讀的內(nèi)存語(yǔ)義:但讀一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無(wú)效,。線程接下來(lái)將從主內(nèi)存中讀取共享變量,。 為了實(shí)現(xiàn)volatile的內(nèi)存語(yǔ)義,編譯器在生成字節(jié)碼時(shí),,會(huì)在指令序列中插入內(nèi)存屏障來(lái)禁止特定類(lèi)型的處理器重排序,。 鎖的內(nèi)存語(yǔ)義鎖的釋放和獲取的內(nèi)存語(yǔ)義當(dāng)線程釋放鎖時(shí):JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。 當(dāng)線程獲取鎖時(shí):JMM會(huì)把該線程對(duì)應(yīng)的本地變量置為無(wú)效,,從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須從主內(nèi)存中讀取共享變量,。
CAS具有volatile讀和volatile寫(xiě)的內(nèi)存語(yǔ)義 公平鎖與非公平鎖的內(nèi)存語(yǔ)義(ReentrantLock為例): 公平鎖和非公平鎖釋放時(shí),最后都要寫(xiě)一個(gè)volatile變量state,。 公平鎖獲取時(shí),,首先會(huì)去讀volatile變量。 非公平鎖獲取時(shí),,首先會(huì)用CAS更新volatile變量,,這個(gè)操作同時(shí)具有volatile讀和volatile寫(xiě)的內(nèi)存語(yǔ)義。
從對(duì)ReentrantLock的分析可以看出,,釋放鎖-獲取鎖的內(nèi)存語(yǔ)義的實(shí)現(xiàn)至少有下面兩種方式: final域的內(nèi)存語(yǔ)義在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè)final域的寫(xiě)入,,與隨后把這個(gè)被構(gòu)造對(duì)象的引用賦值給一個(gè)引用變量,,這兩個(gè)操作之間不能重排序。
初次讀一個(gè)包含final域的對(duì)象的引用,,與隨后初次讀這個(gè)final域,,這兩個(gè)操作之間不能重排序。
JSR-133為什么要增強(qiáng)final的語(yǔ)義,? 在舊的Java內(nèi)存模型中,,一個(gè)最嚴(yán)重的缺陷就是線程可能看到final域的值會(huì)改變,比如,,一個(gè)線程當(dāng)前看到一個(gè)證書(shū)final域的值為0(還未初始化之前的默認(rèn)值),,過(guò)一段時(shí)間之后這個(gè)線程再去讀這個(gè)final域的值時(shí),卻發(fā)現(xiàn)值變成了1(被某個(gè)線程初始化之后的值),。 happens-beforehappens-before定義: 如果一個(gè)操作happens-before另一個(gè)操作,,那么第一個(gè)操作的執(zhí)行結(jié)果將對(duì)第二個(gè)操作可見(jiàn),而且第一個(gè)操作的執(zhí)行順序排在第二個(gè)操作之前,。(是JMM對(duì)程序員的承諾) 兩個(gè)操作之間存在happens-before關(guān)系,,并不意味著Java平臺(tái)的具體實(shí)現(xiàn)必須要按照happens-before關(guān)系執(zhí)行的順序來(lái)執(zhí)行。如果重排序之后的執(zhí)行結(jié)果,,與按happens-before關(guān)系來(lái)執(zhí)行的結(jié)果一直,,那么這種重排序是合法的,。(是JMM對(duì)編譯器和處理器重排序的約束原則)
as-if-serial語(yǔ)義保證單線程內(nèi)程序的執(zhí)行結(jié)果不被改變,happens-before關(guān)系保證正確同步的多線程程序的執(zhí)行結(jié)果不被改變,。 as-if-serial語(yǔ)義給編寫(xiě)單線程程序的程序員創(chuàng)造了一個(gè)幻境:?jiǎn)尉€程程序是按程序的順序來(lái)執(zhí)行的,。happens-before關(guān)系給編寫(xiě)正確同步的多線程的程序員創(chuàng)造了一個(gè)幻境:正確同步的多線程程程是按happens-before指定的順序來(lái)執(zhí)行的。 happens-before規(guī)則: 程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,,happens-before于該線程中的任意后續(xù)操作,。 監(jiān)視器鎖規(guī)則:對(duì)一個(gè)鎖的解鎖,happens-before于隨后對(duì)這個(gè)鎖的加鎖,。 volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫(xiě),,happens-before于任意后續(xù)對(duì)這個(gè)volatile域的讀。 傳遞性:如果A happens-before B,, 且B happens-before C,, 那么A happens-before C。 start()規(guī)則:如果線程A執(zhí)行操作ThreadB.start(),,那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作,。 join()規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回,。
雙重檢查鎖定與延遲初始化public class DoubleCheckedLocking { private static Instance instance;
public static Instance getInsatnce() { if (instance == null) { synchronized(DoubleCheckedLocking.class) { if (instance == null) { instance = new DoubleCheckedLocking(); } } } } }
memory = allocate(); ctorInstance(memory); instance = memory;
上面?zhèn)未a中2和3可能會(huì)被重排序,這種情況下返回的instance引用可能還沒(méi)有初始化完成,。這個(gè)時(shí)候我們要從兩個(gè)方面解決問(wèn)題: 1.不允許2和3重排序,。 2.允許2和3重排序,但不允許其他線程“看到”這個(gè)重排序,。 基于volatile的解決方案public class DoubleCheckedLocking { private volatile static Instance instance;
public static Instance getInsatnce() { if (instance == null) { synchronized(DoubleCheckedLocking.class) { if (instance == null) { instance = new DoubleCheckedLocking(); } } } } }
public class DoubleCheckedLocking { private static class InstanceHolder { public static Instance instance = new Instance(); }
public static Instance getInsatnce() { return InstanceHolder.instance; } }
|