第2章 java并發(fā)機制的底層實現(xiàn)原理
Java中所使用的并發(fā)機制依賴于JVM的實現(xiàn)和CPU的指令。
2.1 volatile的應用
1.volatile的定義與實現(xiàn)原理
volatile可以保證變量的可見性。
如何保證?
volatile變量寫操作時,會引發(fā)兩件事:
1)將當前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。通過緩存一致性協(xié)議來保證寫操作的原子性。
2)這個寫回內(nèi)存的操作會使在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效。
這里的第二點如何實現(xiàn)的呢?
通過緩存一致性協(xié)議,每個處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了,當處理器發(fā)現(xiàn)自己緩存行對應的內(nèi)存地址被修改,就會將當前處理器的緩存行設置成無效狀態(tài)。在下次訪問相同內(nèi)存地址時,強制執(zhí)行緩存行填充。
2.volatile的使用優(yōu)化
略
總結上面這段:http://www.lxweimin.com/p/d03f97ce39d9
2.2?synchronized的實現(xiàn)原理
synchronized實現(xiàn)同步的基礎:Java中的每一個對象都可以作為鎖。
2.2.1 Java對象頭
synchronized用的鎖是存在Java對象頭里的。
2.2.2 鎖的升級與對比
1.偏向鎖
為什么會有偏向鎖?
大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而進入了偏向鎖。
2.輕量級鎖
3.鎖的優(yōu)缺點對比
2.3 原子操作的實現(xiàn)原理
1.術語定義
比較并交換? Compare and Swap? CAS操作需要輸入兩個數(shù)值,一個舊值(期望操作前的值)和一個新值,在操作期間比較舊值有沒有發(fā)生變化,如果沒有發(fā)生變化,才交換成新的值,發(fā)生了變化則不交換。
內(nèi)存順序沖突? Memory order violation? 內(nèi)存順序沖突一般是由假共享引起的,假共享是指多個CPU同時修改同一個緩存行的不同部分而引起其中一個CPU的操作無效,當出現(xiàn)這個內(nèi)存順序沖突時,CPU必須清空流水線。
2.處理器如何實現(xiàn)原子操作
首先處理器會自動保證基本的內(nèi)存操作的原子性。但是復雜的內(nèi)存操作處理器是不能自動保證其原子性的,比如跨總線寬度、跨多個緩存行和跨頁表的訪問。但是,處理器提供總線鎖定和緩存鎖定兩個機制來保證復雜內(nèi)存操作的原子性。
(1) 使用總線鎖定保證原子性
所謂總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞,那么該處理器可以獨占共享內(nèi)存。
(2) 使用緩存鎖保證原子性
使用總線鎖的時候,其他處理器不能操作其他內(nèi)存地址的數(shù)據(jù),這樣,總線鎖定的開銷大。進而,有了緩存鎖定替代總線鎖定來進行優(yōu)化。
所謂“緩存鎖定”是指內(nèi)存區(qū)域如果被緩存在處理器的緩存行中,并且在Lock操作期間被鎖定,那么當它執(zhí)行鎖操作回寫到內(nèi)存時,處理器不在總線上聲言LOCK#信號,而是修改內(nèi)部的內(nèi)存地址,并允許它的緩存一致性機制來保證操作的原子性,因為緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù),當其他處理器回寫已被鎖定的緩存行的數(shù)據(jù)時,會使緩存行無效。
3.Java如何實現(xiàn)原子操作
在Java中可以通過鎖和循環(huán)CAS的方式來實現(xiàn)原子操作。
(1) 使用循環(huán)CAS實現(xiàn)原子操作
自旋CAS實現(xiàn)的基本思路就是循環(huán)進行CAS操作直到成功為止
(2) CAS實現(xiàn)原子操作的三大問題
ABA問題,循環(huán)時間長開銷大,以及只能保證一個共享變量的原子操作。
(3) 使用鎖機制實現(xiàn)原子操作
鎖機制保證了只有獲得鎖的線程才能夠操作鎖定的內(nèi)存區(qū)域。JVM內(nèi)部實現(xiàn)了很多種鎖機制,有偏向鎖、輕量級鎖和互斥鎖。有意思的是除了偏向鎖,JVM實現(xiàn)鎖的方式都用了循環(huán)CAS,即當一個線程想進入同步塊的時候使用循環(huán)CAS的方式來獲取鎖,當它退出同步塊的時候使用循環(huán)CAS釋放鎖。
第3章 Java內(nèi)存模型
3.1 Java內(nèi)存模型的基礎
3.1.1 并發(fā)編程模型的兩個關鍵問題
線程之間如何通信及線程之間如何同步
在共享內(nèi)存的并發(fā)模型里,線程之間共享程序的公共狀態(tài),通過寫-讀內(nèi)存中的公共狀態(tài)進行隱式通信。
同步是指程序中用于控制不同線程間操作發(fā)生相對順序的機制。在共享內(nèi)存并發(fā)模型里,同步是顯式進行的。程序員必須顯式指定某個方法或某段代碼需要在線程之間互斥執(zhí)行。
3.1.2 Java內(nèi)存模型的抽象結構
實例域、靜態(tài)域和數(shù)組元素都存儲在堆內(nèi)存中,堆內(nèi)存在線程之間共享,只有共享的變量才有線程安全性問題。
Java線程之間的通信由Java內(nèi)存模型(本文簡稱為JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。
線程之間的共享變量存儲在主內(nèi)存(Main Memory)中,每個線程都有一個私有的本地內(nèi)存(Local Memory),本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本。
3.1.3 從源代碼到指令序列的重排序
在執(zhí)行程序時,為了提高性能,編譯器和處理器常常會對指令做重排序。
1)編譯器優(yōu)化的重排序。
2)指令級并行的重排序。
3)內(nèi)存系統(tǒng)的重排序。
重排序可能會導致多線程程序出現(xiàn)內(nèi)存可見性問題。
對于編譯器,JMM的編譯器重排序規(guī)則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。
JMM屬于語言級的內(nèi)存模型,它確保在不同的編譯器和不同的處理器平臺之上,通過禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內(nèi)存可見性保證。
3.1.4 并發(fā)編程模型的分類
現(xiàn)代的處理器使用寫緩沖區(qū)臨時保存向內(nèi)存寫入的數(shù)據(jù)。
寫緩沖區(qū)的這種方式有幾個優(yōu)點。
但每個處理器上的寫緩沖區(qū),僅僅對它所在的處理器可見。
這個特性會對內(nèi)存操作的執(zhí)行順序產(chǎn)生重要的影響:處理器對內(nèi)存的讀/寫操作的執(zhí)行順序,不一定與內(nèi)存實際發(fā)生的讀/寫操作順序一致!
由于現(xiàn)代的處理器都會使用寫緩沖區(qū),因此現(xiàn)代的處理器都會允許對寫-讀操作進行重排序。
為了保證內(nèi)存可見性,Java編譯器在生成指令序列的適當位置會插入內(nèi)存屏障指令來禁止特定類型的處理器重排序。JMM把內(nèi)存屏障指令分為4類
LoadLoad屏障:對于這樣的語句Load1; LoadLoad; Load2,在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
StoreStore屏障:對于這樣的語句Store1; StoreStore; Store2,在Store2及后續(xù)寫入操作執(zhí)行前,保證Store1的寫入操作對其它處理器可見。
LoadStore屏障:對于這樣的語句Load1; LoadStore; Store2,在Store2及后續(xù)寫入操作被刷出前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
StoreLoad屏障:對于這樣的語句Store1; StoreLoad; Load2,在Load2及后續(xù)所有讀取操作執(zhí)行前,保證Store1的寫入對所有處理器可見。
StoreLoad Barriers是一個“全能型”的屏障,它同時具有其他3個屏障的效果。現(xiàn)代的多處理器大多支持該屏障(其他類型的屏障不一定被所有處理器支持)。執(zhí)行該屏障開銷會很昂貴,因為當前處理器通常要把寫緩沖區(qū)中的數(shù)據(jù)全部刷新到內(nèi)存中(Buffer Fully Flush)。
3.1.5 happens-before簡介
Java內(nèi)存模型使用happens-before的概念來闡述操作之間的內(nèi)存可見性。在JMM中,如果一個操作執(zhí)行的結果需要對另一個操作可見,那么這兩個操作之間必須要存在happens-before關系。這里提到的兩個操作既可以是在一個線程之內(nèi),也可以是在不同線程之間。
與程序員密切相關的happens-before規(guī)則如下。
程序順序規(guī)則:一個線程中的每個操作,happens-before于該線程中的任意后續(xù)操作。
監(jiān)視器鎖規(guī)則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖。
volatile變量規(guī)則:對一個volatile域的寫,happens-before于任意后續(xù)對這個volatile域的讀。
傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。
兩個操作之間具有happens-before關系,并不意味著前一個操作必須要在后一個操作之前執(zhí)行!happens-before僅僅要求前一個操作(執(zhí)行的結果)對后一個操作可見,且前一個操作按順序排在第二個操作之前(the first is visible to and ordered before the second)。
3.2 重排序
重排序是指編譯器和處理器為了優(yōu)化程序性能而對指令序列進行重新排序的一種手段。
3.2.1 數(shù)據(jù)依賴性
如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數(shù)據(jù)依賴性。
數(shù)據(jù)依賴分為下列3種類型
上面3種情況,只要重排序兩個操作的執(zhí)行順序,程序的執(zhí)行結果就會被改變。
前面提到過,編譯器和處理器可能會對操作做重排序。編譯器和處理器在重排序時,會遵守數(shù)據(jù)依賴性,編譯器和處理器不會改變存在數(shù)據(jù)依賴關系的兩個操作的執(zhí)行順序。
這里所說的數(shù)據(jù)依賴性僅針對單個處理器中執(zhí)行的指令序列和單個線程中執(zhí)行的操作,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮。
3.2.2 as-if-serial語義
as-if-serial語義的意思是:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。
為了遵守as-if-serial語義,編譯器和處理器不會對存在數(shù)據(jù)依賴關系的操作做重排序,因為這種重排序會改變執(zhí)行結果。但是,如果操作之間不存在數(shù)據(jù)依賴關系,這些操作就可能被編譯器和處理器重排序。
as-if-serial語義把單線程程序保護了起來,遵守as-if-serial語義的編譯器、runtime和處理器共同為編寫單線程程序的程序員創(chuàng)建了一個幻覺:單線程程序是按程序的順序來執(zhí)行的。asif-serial語義使單線程程序員無需擔心重排序會干擾他們,也無需擔心內(nèi)存可見性問題。
3.2.3 程序順序規(guī)則
重排序操作A和操作B后的執(zhí)行結果,與操作A和操作B按happens-before順序執(zhí)行的結果一致。在這種情況下,JMM會認為這種重排序并不非法(not illegal),JMM允許這種重排序。
3.2.4 重排序?qū)Χ嗑€程的影響
在單線程程序中,對存在控制依賴的操作重排序,不會改變執(zhí)行結果(這也是as-if-serial語義允許對存在控制依賴的操作做重排序的原因);但在多線程程序中,對存在控制依賴的操作重排序,可能會改變程序的執(zhí)行結果。
3.3 順序一致性
順序一致性內(nèi)存模型是一個理論參考模型,在設計的時候,處理器的內(nèi)存模型和編程語言的內(nèi)存模型都會以順序一致性內(nèi)存模型作為參照。
3.3.1 數(shù)據(jù)競爭與順序一致性
當程序未正確同步時,就可能會存在數(shù)據(jù)競爭。Java內(nèi)存模型規(guī)范對數(shù)據(jù)競爭的定義如下。
在一個線程中寫一個變量,在另一個線程讀同一個變量,而且寫和讀沒有通過同步來排序。
如果程序是正確同步的,程序的執(zhí)行將具有順序一致性(Sequentially Consistent)——即程序的執(zhí)行結果與該程序在順序一致性內(nèi)存模型中的執(zhí)行結果相同。
3.3.2 順序一致性內(nèi)存模型
順序一致性內(nèi)存模型有兩大特性。
1)一個線程中的所有操作必須按照程序的順序來執(zhí)行。
2)(不管程序是否同步)所有線程都只能看到一個單一的操作執(zhí)行順序。在順序一致性內(nèi)存模型中,每個操作都必須原子執(zhí)行且立刻對所有線程可見。
3.3.3 同步程序的順序一致性效果
順序一致性模型中,所有操作完全按程序的順序串行執(zhí)行。而在JMM中,臨界區(qū)內(nèi)的代碼可以重排序。
JMM會在退出臨界區(qū)和進入臨界區(qū)這兩個關鍵時間點做一些特別處理,使得線程在這兩
個時間點具有與順序一致性模型相同的內(nèi)存視圖。雖然線程A在臨界區(qū)內(nèi)做了重排序,但由于監(jiān)視器互斥執(zhí)行的特性,這里的線程B根本無法“觀察”到線程A在臨界區(qū)內(nèi)的重排序。這種重排序既提高了執(zhí)行效率,又沒有改變程序的執(zhí)行結果。
3.3.4 未同步程序的執(zhí)行特性
略
3.4 volatile的內(nèi)存語義
3.4.1 volatile的特性
鎖的happens-before規(guī)則保證釋放鎖和獲取鎖的兩個線程之間的內(nèi)存可見性,這意味著對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入。
鎖的語義決定了臨界區(qū)代碼的執(zhí)行具有原子性。
簡而言之,volatile變量自身具有下列特性。
·可見性。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入。
·原子性:對任意單個volatile變量的讀/寫具有原子性,但類似于volatile++這種復合操作不具有原子性。
3.4.2 volatile寫-讀建立的happens-before關系
從內(nèi)存語義的角度來說,volatile的寫-讀與鎖的釋放-獲取有相同的內(nèi)存效果:volatile寫和鎖的釋放有相同的內(nèi)存語義;volatile讀與鎖的獲取有相同的內(nèi)存語義。
具體的例子看書。
3.4.3 volatile寫-讀的內(nèi)存語義
volatile寫的內(nèi)存語義:當寫一個volatile變量時,JMM會把該線程對應的本地內(nèi)存中的共享變量值刷新到主內(nèi)存。
volatile讀的內(nèi)存語義:當讀一個volatile變量時,JMM會把該線程對應的本地內(nèi)存置為無效。線程接下來將從主內(nèi)存中讀取共享變量。
volatile寫和volatile讀的內(nèi)存語義做個總結
·線程A寫一個volatile變量,實質(zhì)上是線程A向接下來將要讀這個volatile變量的某個線程發(fā)出了(其對共享變量所做修改的)消息。
·線程B讀一個volatile變量,實質(zhì)上是線程B接收了之前某個線程發(fā)出的(在寫這個volatile變量之前對共享變量所做修改的)消息。
·線程A寫一個volatile變量,隨后線程B讀這個volatile變量,這個過程實質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息。
3.4.4 volatile內(nèi)存語義的實現(xiàn)
為了實現(xiàn)volatile內(nèi)存語義,JMM會分別限制這兩種類型的重排序類型。
·當?shù)诙€操作是volatile寫時,不管第一個操作是什么,都不能重排序。這個規(guī)則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之后。
·當?shù)谝粋€操作是volatile讀時,不管第二個操作是什么,都不能重排序。這個規(guī)則確保volatile讀之后的操作不會被編譯器重排序到volatile讀之前。
·當?shù)谝粋€操作是volatile寫,第二個操作是volatile讀時,不能重排序。
為了實現(xiàn)volatile的內(nèi)存語義,編譯器在生成字節(jié)碼時,會在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。
下面是基于保守策略的JMM內(nèi)存屏障插入策略。
·在每個volatile寫操作的前面插入一個StoreStore屏障。
·在每個volatile寫操作的后面插入一個StoreLoad屏障。
·在每個volatile讀操作的后面插入一個LoadLoad屏障。
·在每個volatile讀操作的后面插入一個LoadStore屏障。
上述內(nèi)存屏障插入策略非常保守,但它可以保證在任意處理器平臺,任意的程序中都能得到正確的volatile內(nèi)存語義。
3.4.5 JSR-133為什么要增強volatile的內(nèi)存語義
在舊的內(nèi)存模型中,volatile的寫-讀沒有鎖的釋放-獲所具有的內(nèi)存語義。為了提供一種比鎖更輕量級的線程之間通信的機制,JSR-133專家組決定增強volatile的內(nèi)存語義:嚴格限制編譯器和處理器對volatile變量與普通變量的重排序,確保volatile的寫-讀和鎖的釋放-獲取具有相同的內(nèi)存語義。
3.5 鎖的內(nèi)存語義
3.5.1 鎖的釋放-獲取建立的happens-before關系
鎖除了讓臨界區(qū)互斥執(zhí)行外,還可以讓釋放鎖的線程向獲取同一個鎖的線程發(fā)送消息。
例子看書
3.5.2 鎖的釋放和獲取的內(nèi)存語義
當線程釋放鎖時,JMM會把該線程對應的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。
當線程獲取鎖時,JMM會把該線程對應的本地內(nèi)存置為無效。從而使得被監(jiān)視器保護的臨界區(qū)代碼必須從主內(nèi)存中讀取共享變量。
對比鎖釋放-獲取的內(nèi)存語義與volatile寫-讀的內(nèi)存語義可以看出:鎖釋放與volatile寫有相同的內(nèi)存語義;鎖獲取與volatile讀有相同的內(nèi)存語義。
下面對鎖釋放和鎖獲取的內(nèi)存語義做個總結。
·線程A釋放一個鎖,實質(zhì)上是線程A向接下來將要獲取這個鎖的某個線程發(fā)出了(線程A對共享變量所做修改的)消息。
·線程B獲取一個鎖,實質(zhì)上是線程B接收了之前某個線程發(fā)出的(在釋放這個鎖之前對共享變量所做修改的)消息。
·線程A釋放鎖,隨后線程B獲取這個鎖,這個過程實質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息。
3.5.3 鎖內(nèi)存語義的實現(xiàn)
現(xiàn)在對公平鎖和非公平鎖的內(nèi)存語義做個總結。
·公平鎖和非公平鎖釋放時,最后都要寫一個volatile變量state。
·公平鎖獲取時,首先會去讀volatile變量。
·非公平鎖獲取時,首先會用CAS更新volatile變量,這個操作同時具有volatile讀和volatile寫的內(nèi)存語義。
從本文對ReentrantLock的分析可以看出,鎖釋放-獲取的內(nèi)存語義的實現(xiàn)至少有下面兩種方式。
1)利用volatile變量的寫-讀所具有的內(nèi)存語義。
2)利用CAS所附帶的volatile讀和volatile寫的內(nèi)存語義。
3.5.4 concurrent包的實現(xiàn)
由于Java的CAS同時具有volatile讀和volatile寫的內(nèi)存語義,因此Java線程之間的通信現(xiàn)在有了下面4種方式。
1)A線程寫volatile變量,隨后B線程讀這個volatile變量。
2)A線程寫volatile變量,隨后B線程用CAS更新這個volatile變量。
3)A線程用CAS更新一個volatile變量,隨后B線程用CAS更新這個volatile變量。
4)A線程用CAS更新一個volatile變量,隨后B線程讀這個volatile變量。
如果我們仔細分析concurrent包的源代碼實現(xiàn),會發(fā)現(xiàn)一個通用化的實現(xiàn)模式。
首先,聲明共享變量為volatile。
然后,使用CAS的原子條件更新來實現(xiàn)線程之間的同步。
同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內(nèi)存語義來實現(xiàn)線程之間的通信。
3.6 final域的內(nèi)存語義
3.6.1 final域的重排序規(guī)則
對于final域,編譯器和處理器要遵守兩個重排序規(guī)則。
1)在構造函數(shù)內(nèi)對一個final域的寫入,與隨后把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。
2)初次讀一個包含final域的對象的引用,與隨后初次讀這個final域,這兩個操作之間不能重排序。
3.6.2 寫final域的重排序規(guī)則
寫final域的重排序規(guī)則禁止把final域的寫重排序到構造函數(shù)之外。這個規(guī)則的實現(xiàn)包含下面2個方面。
1)JMM禁止編譯器把final域的寫重排序到構造函數(shù)之外。
2)編譯器會在final域的寫之后,構造函數(shù)return之前,插入一個StoreStore屏障。這個屏障禁止處理器把final域的寫重排序到構造函數(shù)之外。
寫final域的重排序規(guī)則可以確保:在對象引用為任意線程可見之前,對象的final域已經(jīng)被正確初始化過了,而普通域不具有這個保障。
3.6.3 讀final域的重排序規(guī)則
讀final域的重排序規(guī)則是,在一個線程中,初次讀對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操作(注意,這個規(guī)則僅僅針對處理器)。編譯器會在讀final域操作的前面插入一個LoadLoad屏障。
讀final域的重排序規(guī)則可以確保:在讀一個對象的final域之前,一定會先讀包含這個final域的對象的引用。
3.6.4 final域為引用類型
3.6.5 為什么final引用不能從構造函數(shù)內(nèi)“溢出”
3.6.6 final語義在處理器中的實現(xiàn)
3.6.7 JSR-133為什么要增強final的語義
3.7 happens-before
happens-before是JMM最核心的概念。對應Java程序員來說,理解happens-before是理解JMM的關鍵。
3.7.1 JMM的設計
一方面,要為程序員提供足夠強的內(nèi)存可見性保證;另一方面,對編譯器和處理器的限制要盡可能地放松。
·對于會改變程序執(zhí)行結果的重排序,JMM要求編譯器和處理器必須禁止這種重排序。
·對于不會改變程序執(zhí)行結果的重排序,JMM對編譯器和處理器不做要求(JMM允許這種重排序)。
3.7.2 happens-before的定義
用happens-before的概念來指定兩個操作之間的執(zhí)行順序。由于這兩個操作可以在一個線程之內(nèi),也可以是在不同線程之間。因此,JMM可以通過happens-before關系向程序員提供跨線程的內(nèi)存可見性保證(如果A線程的寫操作a與B線程的讀操作b之間存在happensbefore關系,盡管a操作和b操作在不同的線程中執(zhí)行,但JMM向程序員保證a操作將對b操作可見)。
1)如果一個操作happens-before另一個操作,那么第一個操作的執(zhí)行結果將對第二個操作可見,而且第一個操作的執(zhí)行順序排在第二個操作之前。
2)兩個操作之間存在happens-before關系,并不意味著Java平臺的具體實現(xiàn)必須要按照happens-before關系指定的順序來執(zhí)行。如果重排序之后的執(zhí)行結果,與按happens-before關系來執(zhí)行的結果一致,那么這種重排序并不非法(也就是說,JMM允許這種重排序)。
as-if-serial語義和happens-before這么做的目的,都是為了在不改變程序執(zhí)行結果的前提下,盡可能地提高程序執(zhí)行的并行度。
3.7.3 happens-before規(guī)則
1)程序順序規(guī)則:一個線程中的每個操作,happens-before于該線程中的任意后續(xù)操作。
2)監(jiān)視器鎖規(guī)則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖。
3)volatile變量規(guī)則:對一個volatile域的寫,happens-before于任意后續(xù)對這個volatile域的讀。
4)傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。
5)start()規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動線程B),那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作。
6)join()規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回。
3.8 雙重檢查鎖定與延遲初始化
3.8.1 雙重檢查鎖定的由來
3.8.2 問題的根源
3.8.3 基于volatile的解決方案
3.8.4 基于類初始化的解決方案
3.8小結的內(nèi)容易于理解,但不易于筆記。看書
3.9 Java內(nèi)存模型綜述
3.9.1 處理器的內(nèi)存模型
3.9.2 各種內(nèi)存模型之間的關系
JMM是一個語言級的內(nèi)存模型,處理器內(nèi)存模型是硬件級的內(nèi)存模型,順序一致性內(nèi)存模型是一個理論參考模型。
3.9.3 JMM的內(nèi)存可見性保證
3.9.4 JSR-133對舊內(nèi)存模型的修補
增強volatile的內(nèi)存語義。
增強final的內(nèi)存語義。
3.10 本章小結
第4章 Java并發(fā)編程基礎
4.1 線程簡介
4.1.1 什么是線程
4.1.2 為什么要使用多線程
4.1.3 線程優(yōu)先級
4.1.4 線程的狀態(tài)
4.1.5 Daemon線程
4.2 啟動和終止線程
4.2.1 構造線程
4.2.2 啟動線程
4.2.3 理解中斷
4.2.4 過期的suspend()、resume()和stop()
正因為suspend()、resume()和stop()方法帶來的副作用,這些方法才被標注為不建議使用的過期方法,而暫停和恢復操作可以用后面提到的等待/通知機制來替代。
4.2.5 安全地終止線程
4.3 線程間通信
4.3.1 volatile和synchronized關鍵字
關鍵字volatile可以用來修飾字段(成員變量),就是告知程序任何對該變量的訪問均需要從共享內(nèi)存中獲取,而對它的改變必須同步刷新回共享內(nèi)存,它能保證所有線程對變量訪問的可見性。
對于同步塊的實現(xiàn)使用了monitorenter和monitorexit指令,而同步方法則是依靠方法修飾符上的ACC_SYNCHRONIZED來完成的。無論采用哪種方式,其本質(zhì)是對一個對象的監(jiān)視器(monitor)進行獲取,而這個獲取過程是排他的,也就是同一時刻只能有一個線程獲取到由synchronized所保護對象的監(jiān)視器。
任意一個對象都擁有自己的監(jiān)視器,當這個對象由同步塊或者這個對象的同步方法調(diào)用時,執(zhí)行方法的線程必須先獲取到該對象的監(jiān)視器才能進入同步塊或者同步方法,而沒有獲取到監(jiān)視器(執(zhí)行該方法)的線程將會被阻塞在同步塊和同步方法的入口處,進入BLOCKED狀態(tài)。
對象、對象的監(jiān)視器、同步隊列和執(zhí)行線程之間的關系
任意線程對Object(Object由synchronized保護)的訪問,首先要獲得Object的監(jiān)視器。如果獲取失敗,線程進入同步隊列,線程狀態(tài)變?yōu)锽LOCKED。當訪問Object的前驅(qū)(獲得了鎖的線程)釋放了鎖,則該釋放操作喚醒阻塞在同步隊列中的線程,使其重新嘗試對監(jiān)視器的獲取。
4.3.2 等待/通知機制
等待/通知機制,是指一個線程A調(diào)用了對象O的wait()方法進入等待狀態(tài),而另一個線程B調(diào)用了對象O的notify()或者notifyAll()方法,線程A收到通知后從對象O的wait()方法返回,進而執(zhí)行后續(xù)操作。上述兩個線程通過對象O來完成交互,而對象上的wait()和notify/notifyAll()的關系就如同開關信號一樣,用來完成等待方和通知方之間的交互工作。
4.3.3 等待/通知的經(jīng)典范式
4.3.4 管道輸入/輸出流
管道輸入/輸出流和普通的文件輸入/輸出流或者網(wǎng)絡輸入/輸出流不同之處在于,它主要用于線程之間的數(shù)據(jù)傳輸,而傳輸?shù)拿浇闉閮?nèi)存。
4.3.5 Thread.join()的使用
4.3.6 ThreadLocal的使用
4.4 線程應用實例
4.4.1 等待超時模式
4.4.2 一個簡單的數(shù)據(jù)庫連接池示例
4.4.3 線程池技術及其示例
4.4.4 一個基于線程池技術的簡單Web服務器
4.5 本章小結
第5章 Java中的鎖
5.1 Lock接口
使用synchronized關鍵字將會隱式地獲取鎖,但是它將鎖的獲取和釋放固化了,也就是先獲取再釋放。
Lock接口(以及相關實現(xiàn)類)用來實現(xiàn)鎖功能,它提供了與synchronized關鍵字類似的同步功能,只是在使用時需要顯式地獲取和釋放鎖。雖然它缺少了(通過synchronized塊或者方法所提供的)隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等多種synchronized關鍵字所不具備的同步特性。
5.2 隊列同步器
5.2.1 隊列同步器的接口與示例
5.2.2 隊列同步器的實現(xiàn)分析
1.同步隊列
5.4 讀寫鎖
之前提到鎖(如Mutex和ReentrantLock)基本都是排他鎖,這些鎖在同一時刻只允許一個線程進行訪問,而讀寫鎖在同一時刻可以允許多個讀線程訪問,但是在寫線程訪問時,所有的讀線程和其他寫線程均被阻塞。讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,通過分離讀鎖和寫鎖,使得并發(fā)性相比一般的排他鎖有了很大提升。
一般情況下,讀寫鎖的性能都會比排它鎖好,因為大多數(shù)場景讀是多于寫的。在讀多于寫的情況下,讀寫鎖能夠提供比排它鎖更好的并發(fā)性和吞吐量。Java并發(fā)包提供讀寫鎖的實現(xiàn)是ReentrantReadWriteLock
5.6 Condition接口
第6章 Java并發(fā)容器和框架
6.1 ConcurrentHashMap的實現(xiàn)原理與使用
6.1.1 為什么要使用ConcurrentHashMap
在并發(fā)編程中使用HashMap可能導致程序死循環(huán)。而使用線程安全的HashTable效率又非常低下,基于以上兩個原因,便有了ConcurrentHashMap的登場機會。
HashTable容器在競爭激烈的并發(fā)環(huán)境下表現(xiàn)出效率低下的原因是所有訪問HashTable的線程都必須競爭同一把鎖。
ConcurrentHashMap所使用的鎖分段技術。首先將數(shù)據(jù)分成一段一段地存儲,然后給每一段數(shù)據(jù)配一把鎖,當一個線程占用鎖訪問其中一個段數(shù)據(jù)的時候,其他段的數(shù)據(jù)也能被其他線程訪問。
6.1.2 ConcurrentHashMap的結構
ConcurrentHashMap是由Segment數(shù)組結構和HashEntry數(shù)組結構組成。Segment是一種可重入鎖(ReentrantLock),在ConcurrentHashMap里扮演鎖的角色;HashEntry則用于存儲鍵值對數(shù)據(jù)。一個ConcurrentHashMap里包含一個Segment數(shù)組。Segment的結構和HashMap類似,是一種數(shù)組和鏈表結構。一個Segment里包含一個HashEntry數(shù)組,每個HashEntry是一個鏈表結構的元素,每個Segment守護著一個HashEntry數(shù)組里的元素,當對HashEntry數(shù)組的數(shù)據(jù)進行修改時,必須首先獲得與它對應的Segment鎖
6.1.3 ConcurrentHashMap的初始化
1.初始化segments數(shù)組
2.初始化segmentShift和segmentMask
3.初始化每個segment
6.1.4 定位Segment
6.1.5 ConcurrentHashMap的操作
1.get操作
get操作的高效之處在于整個get過程不需要加鎖,除非讀到的值是空才會加鎖重讀。我們知道HashTable容器的get方法是需要加鎖的,那么ConcurrentHashMap的get操作是如何做到不加鎖的呢?原因是它的get方法里將要使用的共享變量都定義成volatile類型,如用于統(tǒng)計當前Segement大小的count字段和用于存儲值的HashEntry的value。定義成volatile的變量,能夠在線程之間保持可見性,能夠被多線程同時讀,并且保證不會讀到過期的值,但是只能被單線程寫(有一種情況可以被多線程寫,就是寫入的值不依賴于原值),在get操作里只需要讀不需要寫共享變量count和value,所以可以不用加鎖。之所以不會讀到過期的值,是因為根據(jù)Java內(nèi)存模型的happen before原則,對volatile字段的寫入操作先于讀操作,即使兩個線程同時修改和獲取volatile變量,get操作也能拿到最新的值,這是用volatile替換鎖的經(jīng)典應用場景。
2.put操作
插入操作需要經(jīng)歷兩個
步驟,第一步判斷是否需要對Segment里的HashEntry數(shù)組進行擴容,第二步定位添加元素的位置,然后將其放在HashEntry數(shù)組里。
為了高效,ConcurrentHashMap不會對整個容器進行擴容,而只對某個segment進行擴容。
3.size操作
如果要統(tǒng)計整個ConcurrentHashMap里元素的大小,就必須統(tǒng)計所有Segment里元素的大小后求和。
因為在累加count操作過程中,之前累加過的count發(fā)生變化的幾率非常小,所以ConcurrentHashMap的做法是先嘗試2次通過不鎖住Segment的方式來統(tǒng)計各個Segment大小,如果統(tǒng)計的過程中,容器的count發(fā)生了變化,則再采用加鎖的方式來統(tǒng)計所有Segment的大小。
可以看看書,這里介紹的并不復雜。
6.2 ConcurrentLinkedQueue
6.2.1 ConcurrentLinkedQueue的結構
ConcurrentLinkedQueue由head節(jié)點和tail節(jié)點組成,每個節(jié)點(Node)由節(jié)點元素(item)和指向下一個節(jié)點(next)的引用組成,節(jié)點與節(jié)點之間就是通過這個next關聯(lián)起來,從而組成一張鏈表結構的隊列。默認情況下head節(jié)點存儲的元素為空,tail節(jié)點等于head節(jié)點。
6.2.2 入隊列
1.入隊列的過程
入隊列就是將入隊節(jié)點添加到隊列的尾部。
2.定位尾節(jié)點
3.設置入隊節(jié)點為尾節(jié)點
6.2.3 出隊列
6.3 Java中的阻塞隊列
6.3.1 什么是阻塞隊列
1)支持阻塞的插入方法:意思是當隊列滿時,隊列會阻塞插入元素的線程,直到隊列不滿。
2)支持阻塞的移除方法:意思是在隊列為空時,獲取元素的線程會等待隊列變?yōu)榉强铡?/p>
6.3.2 Java里的阻塞隊列
有7種
6.3.3 阻塞隊列的實現(xiàn)原理
6.4 Fork/Join框架
6.4.1 什么是Fork/Join框架
6.4.2 工作竊取算法
6.4.3 Fork/Join框架的設計
步驟1 分割任務。
步驟2 執(zhí)行任務并合并結果。
6.4.4 使用Fork/Join框架
6.4.5 Fork/Join框架的異常處理
6.4.6 Fork/Join框架的實現(xiàn)原理
6.5 本章小結
第7章 Java中的13個原子操作類
atomic包有四種類型:
原子更新基本類型、原子更新數(shù)組、原子更新引用和原子更新屬性(字段)。
7.1 原子更新基本類型類
7.2 原子更新數(shù)組
7.3 原子更新引用類型
7.4 原子更新字段類
7.5 本章小結
本章比較簡單,也不是重點。可以,優(yōu)先級低。
第8章 Java中的并發(fā)工具類
8.1 等待多線程完成的CountDownLatch
CountDownLatch允許一個或多個線程等待其他線程完成操作。
8.2 同步屏障CyclicBarrier
8.2.1 CyclicBarrier簡介
8.2.2 CyclicBarrier的應用場景
8.2.3 CyclicBarrier和CountDownLatch的區(qū)別
8.4 線程間交換數(shù)據(jù)的Exchanger