Java并發(fā)編程的藝術

第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

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



轉自:https://blog.csdn.net/bohu83/article/details/51675311

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

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