日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

讀書筆記之Java并發(fā)原理與JMM

 黃家v少 2018-04-12

備注:本文是《Java并發(fā)編程的藝術(shù)》一書的讀書筆記。

1
Java并發(fā)機(jī)制的底層原理實(shí)現(xiàn)

volatile的應(yīng)用

 volatile是輕量級(jí)的synchronized,他在多處理器開發(fā)中保證了共享變量的“可見性”??梢娦缘囊馑际钱?dāng)一個(gè)線程修改一個(gè)共享變量時(shí),另外一個(gè)線程能讀到這個(gè)修改的值。如果volatile變量修飾符使用恰當(dāng)?shù)脑?,它比synchronized的使用和執(zhí)行成本更低,因?yàn)樗粫?huì)引起線程上下文的切換和調(diào)度。


synchronized的實(shí)現(xiàn)原理與應(yīng)用

 synchronized實(shí)現(xiàn)同步的基礎(chǔ):Java中的每一個(gè)對(duì)象都可以作為鎖。具體表現(xiàn)為以下3中形勢* 

 1.對(duì)于普通同步方法,鎖是當(dāng)前實(shí)例對(duì)象。
 2.對(duì)于靜態(tài)同步方法,鎖是當(dāng)前類的Class對(duì)象。
 3.對(duì)于同步方法塊,鎖是Synchronized括號(hào)里配置的對(duì)象。
 當(dāng)一個(gè)線程試圖訪問同步代碼塊時(shí),它首先必須得到鎖,推出或拋出異常時(shí)必須釋放鎖。  

偏向鎖

  當(dāng)一個(gè)線程訪問同步塊并獲取鎖時(shí),會(huì)在對(duì)象頭和棧幀中的鎖記錄里存儲(chǔ)鎖偏向的線程ID,以后該線程在進(jìn)入和退出同步塊時(shí)不需要進(jìn)行CAS操作來加鎖和解鎖,只需簡單地測試一下對(duì)象頭的Mark Word里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖。如果測試成功,表示線程已經(jīng)獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標(biāo)識(shí)是否設(shè)置成1(表示當(dāng)前是偏向鎖):如果沒有設(shè)置,則使用CAS競爭鎖;如果設(shè)置了,則嘗試使用CAS將對(duì)象頭的偏向鎖指向當(dāng)前線程。偏向鎖使用了一種等到競爭出現(xiàn)才釋放鎖的機(jī)制,所以當(dāng)其他線程嘗試競爭偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖。偏向鎖的撤銷,需要等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒有正在執(zhí)行的字節(jié)碼)。  

輕量級(jí)鎖

  線程在執(zhí)行同步塊之前,JVM會(huì)先在當(dāng)前線程的棧幀中創(chuàng)建用于存儲(chǔ)鎖記錄的空間,并將對(duì)象頭中的Mark Word復(fù)制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試CAS將對(duì)象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當(dāng)前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當(dāng)前線程便嘗試使用自旋來獲取鎖。輕量級(jí)解鎖時(shí),會(huì)使用原子的CAS操作將Displaced Mark Word 替換回到對(duì)象頭,如果成功,則表示沒有競爭發(fā)生。如果失敗,表示當(dāng)前鎖存在競爭,鎖就會(huì)膨脹成重量級(jí)鎖。因?yàn)樽孕龝?huì)消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦確認(rèn)升級(jí)成重量級(jí)鎖,就不會(huì)再恢復(fù)到輕量級(jí)鎖狀態(tài)。當(dāng)鎖處于這個(gè)狀態(tài)下,其他線程試圖獲取鎖時(shí),都會(huì)被阻塞住,當(dāng)持有鎖的線程釋放鎖之后會(huì)喚醒這些線程,被喚醒的線程就會(huì)進(jìn)行新一輪的奪鎖之爭。  

鎖的優(yōu)缺點(diǎn)對(duì)比

                                                                                                                                                                       

優(yōu)點(diǎn)缺點(diǎn)適用場景
偏向鎖加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法相比僅存在納秒級(jí)的差距如果線程間存在鎖競爭,會(huì)帶來額外的鎖撤銷的消耗適用于只有一個(gè)線程訪問同步塊場景
輕量級(jí)鎖競爭的線程不會(huì)阻塞,提高了線程的響應(yīng)速度如果始終得不到鎖競爭的線程,使用自旋會(huì)消耗CPU追求響應(yīng)時(shí)間
同步塊執(zhí)行速度非常快
重量級(jí)鎖線程競爭不適用自旋,不會(huì)消耗CPU線程阻塞,響應(yīng)時(shí)間緩慢追求吞吐量
同步塊執(zhí)行速度較長

原子操作的實(shí)現(xiàn)原理

 

原子(atomic)本意是“不能被進(jìn)一步分割的最小粒子“,而原子操作(atomic operation)意為“不可被中斷的一個(gè)或一系列操作“。  

術(shù)語定義

                                                                                                                                                               

術(shù)語名稱英文解釋
緩存行Cache line緩存的最小操作單位
比較并交換Compare and SwapCAS操作需要輸入兩個(gè)數(shù)值,一個(gè)舊值(期望操作前的值)和一個(gè)新值,在操作期間先比較舊值有沒有發(fā)生變化,如果沒有發(fā)生變化,才交換成新值,發(fā)生了變化則不交換
CPU流水線CPU pipelineCPU流水線的工作方式就像工業(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的操作無效,當(dāng)出現(xiàn)這個(gè)內(nèi)存順序沖突時(shí),CPU必須清空流水線

處理器如何實(shí)現(xiàn)原子操作

 處理器提供總線鎖定和緩存鎖定兩個(gè)機(jī)制來保證復(fù)雜內(nèi)存操作的原子性

(1)使用總線鎖保證原子性:第一機(jī)制是通過總線鎖保證原子性。保證CPU1讀改寫共享變量的時(shí)候,CPU2不能操作緩存了該共享變量內(nèi)存地址的緩存。所謂總線鎖就是使用處理器提供的一個(gè)LOCK#信號(hào),當(dāng)一個(gè)處理器在總線上輸出此信號(hào)時(shí),其他處理器的請(qǐng)求將被阻塞住,那么該處理器可以獨(dú)享共享內(nèi)存。
(2)使用緩存鎖保證原子性:第二個(gè)機(jī)制是通過緩存鎖定來保證原子性。在同一時(shí)刻,我們只需保證對(duì)某個(gè)內(nèi)存地址的操作是原子性即可,但總線鎖定把CPU和內(nèi)存之間的通信鎖住了,這使得鎖定期間,其他處理器不能操作其它內(nèi)存地址的數(shù)據(jù),所以總線鎖定的開銷比較大,目前處理器在某些場景下使用緩存鎖定代替總線鎖定來進(jìn)行優(yōu)化。所謂“緩存鎖定”是指內(nèi)存區(qū)域如果被緩存在處理器的緩存行中,并且在Lock操作期間被鎖定,那么當(dāng)其他執(zhí)行鎖操作回寫到內(nèi)存時(shí),處理器不在總線上聲言LOCK#信號(hào),而是修改內(nèi)部的內(nèi)存地址,并允許它的緩存一致性機(jī)制來保證操作的原子性,因?yàn)榫彺嬉恢滦詸C(jī)制會(huì)阻止同時(shí)修改由兩個(gè)以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù),到其他處理器回寫已被鎖定的緩存行數(shù)據(jù)時(shí),會(huì)使緩存行無效。
但是有兩種情況下處理器不回使用緩存鎖定:當(dāng)操作的數(shù)據(jù)不能被緩存在處理器內(nèi)部,或操作的數(shù)據(jù)跨多個(gè)緩存行時(shí),則處理器會(huì)調(diào)用總線鎖定。當(dāng)有些處理器不支持緩存鎖定。

2
Java如何實(shí)現(xiàn)原子操作


(1)使用循環(huán)CAS實(shí)現(xiàn)原子操作:JVM中的CAS操作正是利用了處理器提供的CMPXCHG指令實(shí)現(xiàn)的。自旋CAS實(shí)現(xiàn)的基本思路就是循環(huán)進(jìn)行CAS操作直到成功為止。從Java1.5開始,JDK的并發(fā)包里提供了一些類來支持原子操作,如AtomicBoolean。
(2)CAS實(shí)現(xiàn)原子操作的三大問題ABA問題,因?yàn)镃AS需要在操作值的時(shí)候,檢查值有沒有發(fā)生變化,如果沒有發(fā)生變化則更新,但是如果一個(gè)值原來是A,變成了B,又變成了A,那么使用CAS進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)它的值沒有變化,但是實(shí)際上卻變化了。ABA問題的解決思路是使用版本號(hào),在變量前追加版本號(hào),每次變量更新的時(shí)候把版本號(hào)加1,那么A-B-A就會(huì)變成了1A-2B-3A。從JAVA1.5開始,JDK的Atomic包里提供了一個(gè)類AtomicStampedreference來解決ABA問題。這個(gè)類的compareAndSet方法的作用是首先檢查當(dāng)前引用是否等于預(yù)期引用,并且檢查當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志,如果全部相等,則以原子方式將該引用和該標(biāo)識(shí)的值設(shè)置為給定的更新值。循環(huán)時(shí)間長開銷大,自旋CAS如果長時(shí)間不成功,會(huì)給CPU帶來非常大的執(zhí)行開銷。如果JVM能支持處理器提供的pause指令,那么效率會(huì)有一定的提升。pause指令有兩個(gè)作用:第一,它可以延遲流水線執(zhí)行指令,使CPU不會(huì)消耗過多的執(zhí)行資源,延遲的時(shí)間取決于具體的版本,在一些處理器上延遲時(shí)間是零;第二,它可以避免在退出循環(huán)的時(shí)候因內(nèi)存順序沖突而引起CPU流水線被清空,從而提高CPU的執(zhí)行效率。只能保證一個(gè)共享變量的原子性,當(dāng)對(duì)一個(gè)共享變量執(zhí)行操作時(shí),我們可以使用循環(huán)CAS的方式來保證原子操作,但是對(duì)多個(gè)共享變量操作時(shí),循環(huán)CAS就無法保證操作的原子性,這個(gè)時(shí)候就可以用鎖。還有一個(gè)取巧的辦法,就是把多個(gè)共享變量合并成一個(gè)共享變量來操作。Java1.5以后JDK提供了AtomicReference類來保證引用對(duì)象之間的原子性,就可以把多個(gè)變量放在一個(gè)對(duì)象里來進(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的方式來獲取鎖,當(dāng)它退出同步塊的時(shí)候使用循環(huán)CAS釋放鎖。

3
Java內(nèi)存模型

Java內(nèi)存模型的基礎(chǔ)

 在并發(fā)編程中,需要處理兩個(gè)關(guān)鍵問題:線程之間如何通信及線程之間如何同步(這里的線程是指并發(fā)執(zhí)行的活動(dòng)實(shí)體)。通行是指線程之間以何種機(jī)制來交換信息。在命令式編程中,線程之間的通信機(jī)制有兩種:內(nèi)存共享消息傳遞
   

同步 是指程序中用于控制不同線程間操作發(fā)生相對(duì)順序的機(jī)制。在共享內(nèi)存并發(fā)模型里,同步是顯性進(jìn)行的。程序員需要顯性執(zhí)行某個(gè)方法或某段代碼需要在線程之間互斥執(zhí)行。在消息傳遞的并發(fā)模型里,由于消息的發(fā)送必須在消息的接受之前,因此同步是隱形進(jìn)行的。

Java并發(fā)采用的是共享內(nèi)存模型。

JMM通過控制主內(nèi)存與每個(gè)線程的本地內(nèi)存之間的交互,來為Java程序員提供內(nèi)存可見性。

從源代碼到指令序列的重排序

在執(zhí)行程序時(shí),為了提高性能,編譯器和處理器常常會(huì)對(duì)指令做重排序,重排序分3種類型。
1)編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序。
2)指令級(jí)并行的重排序?,F(xiàn)代處理器采用了指令級(jí)并行技術(shù)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
3)內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。

happens-before簡介

 JSR-133(JDK1.5開始Java使用的新的內(nèi)存模型)使用 happens-before 的概念來闡述操作之間的內(nèi)存可見性。在JMM中,如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見,那么這兩個(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域的寫,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è)操作可見,且前一個(gè)操作按順序排在第二個(gè)操作之前。

重排序

 重排序是指編譯器和處理器為了優(yōu)化程序性能而對(duì)指令序列進(jìn)行重新排序的一種手段。

as-if-serial語義

 as-if-serial的語義是:不管怎么重排序,單線程程序執(zhí)行的結(jié)果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。

volatile的內(nèi)存語義

  • 可見性。對(duì)一個(gè)volatile變量的讀,總是能看到任意線程對(duì)這個(gè)volatile變量最后的寫入。

  • 原子性。對(duì)任意單個(gè)volatile變量的讀/寫具有原子性,但類似于volatile++這種復(fù)合操作不具有原子性。


volatile寫-讀的內(nèi)存語義:當(dāng)寫一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存。
volatile讀的內(nèi)存語義:但讀一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無效。線程接下來將從主內(nèi)存中讀取共享變量。
為了實(shí)現(xiàn)volatile的內(nèi)存語義,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。

鎖的內(nèi)存語義

鎖的釋放和獲取的內(nèi)存語義

  • 當(dāng)線程釋放鎖時(shí):JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。  

  • 當(dāng)線程獲取鎖時(shí):JMM會(huì)把該線程對(duì)應(yīng)的本地變量置為無效,從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須從主內(nèi)存中讀取共享變量。


CAS具有volatile讀和volatile寫的內(nèi)存語義

公平鎖與非公平鎖的內(nèi)存語義(ReentrantLock為例):

  • 公平鎖和非公平鎖釋放時(shí),最后都要寫一個(gè)volatile變量state。  

  • 公平鎖獲取時(shí),首先會(huì)去讀volatile變量。  

  • 非公平鎖獲取時(shí),首先會(huì)用CAS更新volatile變量,這個(gè)操作同時(shí)具有volatile讀和volatile寫的內(nèi)存語義。


從對(duì)ReentrantLock的分析可以看出,釋放鎖-獲取鎖的內(nèi)存語義的實(shí)現(xiàn)至少有下面兩種方式:

  • 利用volatile變量的寫-讀所具有的內(nèi)存語義。

  • 利用CAS所附帶的volatile讀和volatile寫的內(nèi)存語義。  


final域的內(nèi)存語義

  • 在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè)final域的寫入,與隨后把這個(gè)被構(gòu)造對(duì)象的引用賦值給一個(gè)引用變量,這兩個(gè)操作之間不能重排序。


  • 初次讀一個(gè)包含final域的對(duì)象的引用,與隨后初次讀這個(gè)final域,這兩個(gè)操作之間不能重排序。


JSR-133為什么要增強(qiáng)final的語義?
在舊的Java內(nèi)存模型中,一個(gè)最嚴(yán)重的缺陷就是線程可能看到final域的值會(huì)改變,比如,一個(gè)線程當(dāng)前看到一個(gè)證書final域的值為0(還未初始化之前的默認(rèn)值),過一段時(shí)間之后這個(gè)線程再去讀這個(gè)final域的值時(shí),卻發(fā)現(xiàn)值變成了1(被某個(gè)線程初始化之后的值)。

happens-before

happens-before定義:  

  • 如果一個(gè)操作happens-before另一個(gè)操作,那么第一個(gè)操作的執(zhí)行結(jié)果將對(duì)第二個(gè)操作可見,而且第一個(gè)操作的執(zhí)行順序排在第二個(gè)操作之前。(是JMM對(duì)程序員的承諾)

  • 兩個(gè)操作之間存在happens-before關(guān)系,并不意味著Java平臺(tái)的具體實(shí)現(xiàn)必須要按照happens-before關(guān)系執(zhí)行的順序來執(zhí)行。如果重排序之后的執(zhí)行結(jié)果,與按happens-before關(guān)系來執(zhí)行的結(jié)果一直,那么這種重排序是合法的。(是JMM對(duì)編譯器和處理器重排序的約束原則)


 as-if-serial語義保證單線程內(nèi)程序的執(zhí)行結(jié)果不被改變,happens-before關(guān)系保證正確同步的多線程程序的執(zhí)行結(jié)果不被改變。 

 as-if-serial語義給編寫單線程程序的程序員創(chuàng)造了一個(gè)幻境:單線程程序是按程序的順序來執(zhí)行的。happens-before關(guān)系給編寫正確同步的多線程的程序員創(chuàng)造了一個(gè)幻境:正確同步的多線程程程是按happens-before指定的順序來執(zhí)行的。

happens-before規(guī)則:

  • 程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happens-before于該線程中的任意后續(xù)操作。

  • 監(jiān)視器鎖規(guī)則:對(duì)一個(gè)鎖的解鎖,happens-before于隨后對(duì)這個(gè)鎖的加鎖。

  • volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫,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();
               }
           }
       }
   }
}
  • 雙重檢查是比較常見的一種延遲初始化方案,但是還是會(huì)存在一些問題:

memory = allocate();    
ctorInstance(memory);  
instance = memory;


上面?zhèn)未a中2和3可能會(huì)被重排序,這種情況下返回的instance引用可能還沒有初始化完成。這個(gè)時(shí)候我們要從兩個(gè)方面解決問題:
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;
   }
}

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多