Java的并發采用的是共享內存模型(而非消息傳遞模型),線程之間共享程序的公共狀態,線程之間通過寫-讀內存中的公共狀態來隱式進行通信。多個線程之間是不能直接傳遞數據交互的,它們之間的交互只能通過共享變量來實現
同步是顯式進行的。程序員必須顯式指定某個方法或某段代碼需要在線程之間互斥執行。
1、多線程通信
1.1 內存模型
Java線程之間的通信由Java內存模型(JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。
從抽象的角度來看,JMM定義了線程和主內存之間的抽象關系:線程之間的共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,并不真實存在,它涵蓋了緩存,寫緩沖區,寄存器以及其他的硬件和編譯器優化。Java內存模型的抽象示意圖如下:
線程間通信的步驟:
- 首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去。
- 然后,線程B到主內存中去讀取線程A之前已更新過的共享變量。
- 本地內存A和B有主內存中共享變量x的副本。
- 假設初始時,這三個內存中的x值都為0。線程A在執行時,把更新后的x值(假設值為1)臨時存放在自己的本地內存A中。
- 當線程A和線程B需要通信時(如何激發?–隱式),線程A首先會把自己本地內存中修改后的x值刷新到主內存中,此時主內存中的x值變為了1。
- 隨后,線程B到主內存中去讀取線程A更新后的x值,此時線程B的本地內存的x值也變為了1。
從整體來看,這兩個步驟實質上是線程A在向線程B發送消息,而且這個通信過程必須要經過主內存。JMM通過控制主內存與每個線程的本地內存之間的交互,來為java程序員提供內存可見性保證。
1.2 可見性、有序性
例如在多個線程之間共享了Count類的一個對象,這個對象是被創建在主內存(堆內存)中,每個線程都有自己的本地內存(線程棧),工作內存存儲了主內存Count對象的一個副本,當線程操作Count對象時,首先從主內存復制Count對象到工作內存中,然后執行代碼count.count(),改變了num值,最后用工作內存Count刷新主內存Count。
當一個對象在多個內存中都存在副本時,如果一個內存修改了共享變量,其它線程也應該能夠看到被修改后的值,此為可見性。
一個運算賦值操作并不是一個原子性操作,多個線程執行時,CPU對線程的調度是隨機的,我們不知道當前程序被執行到哪步就切換到了下一個線程,一個最經典的例子就是銀行匯款問題,一個銀行賬戶存款100,這時一個人從該賬戶取10元,同時另一個人向該賬戶匯10元,那么余額應該還是100。那么此時可能發生這種情況,A線程負責取款,B線程負責匯款,A從主內存讀到100,B從主內存讀到100,A執行減10操作,并將數據刷新到主內存,這時主內存數據100-10=90,而B內存執行加10操作,并將數據刷新到主內存,最后主內存數據100+10=110,顯然這是一個嚴重的問題,我們要保證A線程和B線程有序執行,先取款后匯款或者先匯款后取款,此為有序性。
1.3 synchronized與volatile
一個線程執行互斥代碼過程如下:
- 獲得同步鎖;
- 清空工作內存;
- 從主內存拷貝對象副本到工作內存;
- 執行代碼(計算或者輸出等);
- 刷新主內存數據;
- 釋放同步鎖。
所以,synchronized既保證了多線程的并發有序性,又保證了多線程的內存可見性。
volatile是第二種Java多線程同步的手段,根據JLS的說法,一個變量可以被volatile修飾,在這種情況下內存模型確保所有線程可以看到一致的變量值
class Test { static volatile int i = 0, j = 0; static void one() { i++; j++; } static void two() { System.out.println("i=" + i + " j=" + j); } }
加上volatile可以將共享變量i和j的改變直接響應到主內存中,這樣保證了i和j的值可以保持一致,然而我們不能保證執行two方法的線程是在i和j執行到什么程度獲取到的,所以volatile可以保證內存可見性,不能保證并發有序性。
如果沒有volatile,則代碼執行過程如下:
-
將變量i從主內存拷貝到工作內存;
-
刷新主內存數據;
- 改變i的值;
-
將變量j從主內存拷貝到工作內存;
-
刷新主內存數據;
- 改變j的值;
2、重排序
JMM屬于語言級的內存模型,它確保在不同的編譯器和不同的處理器平臺之上,通過禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內存可見性保證。
對于編譯器沖排序,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。
對于處理器重排序,JMM的處理器重排序規則會要求java編譯器在生成指令序列時,插入特定類型的內存屏障(memory barriers,intel稱之為memory fence)指令,通過內存屏障指令來禁止特定類型的處理器重排序(不是所有的處理器重排序都要禁止)。
引申:
在執行程序時為了提高性能,編譯器和處理器常常會對指令做重排序。重排序分三種類型:
- 編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
- 指令級并行的重排序?,F代處理器采用了指令級并行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
- 內存系統的重排序。由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。
上述的1屬于編譯器重排序,2和3屬于處理器重排序。這些重排序都可能會導致多線程程序出現內存可見性問題。
2.1 數據依賴性
如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數據依賴性。數據依賴分下列三種類型:
名稱 | 代碼示例 | 說明 |
寫后讀 | a = 1;b = a; | 寫一個變量之后,再讀這個位置。 |
寫后寫 | a = 1;a = 2; | 寫一個變量之后,再寫這個變量。 |
讀后寫 | a = b;b = 1; | 讀一個變量之后,再寫這個變量。 |
上面三種情況,只要重排序兩個操作的執行順序,程序的執行結果將會被改變。
前面提到過,編譯器和處理器可能會對操作做重排序。編譯器和處理器在重排序時,會遵守數據依賴性,編譯器和處理器不會改變存在數據依賴關系的兩個操作的執行順序。
注意,這里所說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操作,不同處理器之間和不同線程之間的數據依賴性不被編譯器和處理器考慮。
2.2 as-if-serial語義
as-if-serial語義的意思指:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執行結果不能被改變。編譯器,runtime 和處理器都必須遵守as-if-serial語義。
【例】
double pi = 3.14; //A double r = 1.0; //B double area = pi * r * r; //C
上面三個操作的數據依賴關系如下圖所示:
如上圖所示,A和C之間存在數據依賴關系,同時B和C之間也存在數據依賴關系。因此在最終執行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的結果將會被改變)。但A和B之間沒有數據依賴關系,編譯器和處理器可以重排序A和B之間的執行順序。下圖是該程序的兩種執行順序:
as-if-serial語義把單線程程序保護了起來,遵守as-if-serial語義的編譯器,runtime 和處理器共同為編寫單線程程序的程序員創建了一個幻覺:單線程程序是按程序的順序來執行的。as-if-serial語義使單線程程序員無需擔心重排序會干擾他們,也無需擔心內存可見性問題。
2.3 happens-before
從JDK5開始,java使用新的JSR -133內存模型。JSR-133提出了happens-before的概念,通過這個概念來闡述操作之間的內存可見性。如果一個操作執行的結果需要對另一個操作可見,那么這兩個操作之間必須存在happens-before關系。這里提到的兩個操作既可以是在一個線程之內,也可以是在不同線程之間。 與程序員密切相關的happens-before規則如下:
- 程序順序規則:一個線程中的每個操作,happens- before 于該線程中的任意后續操作。
- 監視器鎖規則:對一個監視器鎖的解鎖,happens- before 于隨后對這個監視器鎖的加鎖。
- volatile變量規則:對一個volatile域的寫,happens- before 于任意后續對這個volatile域的讀。
- 傳遞性:如果A happens- before B,且B happens- before C,那么A happens- before C。
注意,兩個操作之間具有happens-before關系,并不意味著前一個操作必須要在后一個操作之前執行!happens-before僅僅要求前一個操作(執行的結果)對后一個操作可見,且前一個操作按順序排在第二個操作之前(the first is visible to and ordered before the second)。happens- before的定義很微妙,后文會具體說明happens-before為什么要這么定義。
【例】根據happens- before的程序順序規則,上面計算圓的面積的示例代碼存在三個happens- before關系:
- A happens- before B;
- B happens- before C;
- A happens- before C;
這里的第3個happens- before關系,是根據happens- before的傳遞性推導出來的。
這里A happens- before B,但實際執行時B卻可以排在A之前執行(看上面的重排序后的執行順序)。A happens- before B,JMM并不要求A一定要在B之前執行。JMM僅僅要求前一個操作(執行的結果)對后一個操作可見,且前一個操作按順序排在第二個操作之前。這里操作A的執行結果不需要對操作B可見;而且重排序操作A和操作B后的執行結果,與操作A和操作B按happens- before順序執行的結果一致。在這種情況下,JMM會認為這種重排序并不非法(not illegal),JMM允許這種重排序。
在計算機中,軟件技術和硬件技術有一個共同的目標:在不改變程序執行結果的前提下,盡可能的開發并行度。編譯器和處理器遵從這一目標,從happens- before的定義我們可以看出,JMM同樣遵從這一目標。
2.4 重排序對多線程的影響
現在讓我們來看看,重排序是否會改變多線程程序的執行結果?!纠浚?/p>
class ReorderExample { int a = 0; boolean flag = false; public void writer() { a = 1; //1 flag = true; //2 } Public void reader() { if (flag) { //3 int i = a * a; //4 …… } } }
flag變量是個標記,用來標識變量a是否已被寫入。這里假設有兩個線程A和B,A首先執行writer()方法,隨后B線程接著執行reader()方法。線程B在執行操作4時,能否看到線程A在操作1對共享變量a的寫入?
答案是:不一定能看到。
由于操作1和操作2沒有數據依賴關系,編譯器和處理器可以對這兩個操作重排序;同樣,操作3和操作4沒有數據依賴關系(?),編譯器和處理器也可以對這兩個操作重排序。讓我們先來看看,當操作1和操作2重排序時,可能會產生什么效果?請看下面的程序執行時序圖:
如上圖所示,操作1和操作2做了重排序。程序執行時,線程A首先寫標記變量flag,隨后線程B讀這個變量。由于條件判斷為真,線程B將讀取變量a。此時,變量a還根本沒有被線程A寫入,在這里多線程程序的語義被重排序破壞了!
下面再讓我們看看,當操作3和操作4重排序時會產生什么效果(借助這個重排序,可以順便說明控制依賴性)。下面是操作3和操作4重排序后,程序的執行時序圖:
在程序中,操作3和操作4存在控制依賴關系。當代碼中存在控制依賴性時,會影響指令序列執行的并行度。為此,編譯器和處理器會采用猜測(Speculation)執行來克服控制相關性對并行度的影響。以處理器的猜測執行為例,執行線程B的處理器可以提前讀取并計算a*a,然后把計算結果臨時保存到一個名為重排序緩沖(reorder buffer ROB)的硬件緩存中。當接下來操作3的條件判斷為真時,就把該計算結果寫入變量i中。
從圖中我們可以看出,猜測執行實質上對操作3和4做了重排序。重排序在這里破壞了多線程程序的語義!
在單線程程序中,對存在控制依賴的操作重排序,不會改變執行結果(這也是as-if-serial語義允許對存在控制依賴的操作做重排序的原因);但在多線程程序中,對存在控制依賴的操作重排序,可能會改變程序的執行結果。
3、順序一致性
3.1 數據競爭
當程序未正確同步時,就會存在數據競爭。java內存模型規范對數據競爭的定義如下:
- 在一個線程中寫一個變量,
- 在另一個線程讀同一個變量,
- 而且寫和讀沒有通過同步來排序。
當代碼中包含數據競爭時,程序的執行往往產生違反直覺的結果(前一章的示例正是如此)。如果一個多線程程序能正確同步,這個程序將是一個沒有數據競爭的程序。
JMM對正確同步的多線程程序的內存一致性做了如下保證:
- 如果程序是正確同步的,程序的執行將具有順序一致性(sequentially consistent)——即程序的執行結果與該程序在順序一致性內存模型中的執行結果相同。這里的同步是指廣義上的同步,包括對常用同步原語(lock,volatile和final)的正確使用。
3.2 順序一致性內存模型
順序一致性內存模型有兩大特性:
- 一個線程中的所有操作必須按照程序的順序來執行。
- (不管程序是否同步)所有線程都只能看到一個單一的操作執行順序。在順序一致性內存模型中,每個操作都必須原子執行且立刻對所有線程可見。
順序一致性內存模型為程序員提供的視圖如下。在概念上,順序一致性模型有一個單一的全局內存,這個內存通過一個左右擺動的開關可以連接到任意一個線程。同時,每一個線程必須按程序的順序來執行內存讀/寫操作。在任意時間點最多只能有一個線程可以連接到內存。當多個線程并發執行時,圖中的開關裝置能把所有線程的所有內存讀/寫操作串行化。
為了更好的理解,下面我們通過兩個示意圖來對順序一致性模型的特性做進一步的說明。
假設有兩個線程A和B并發執行。其中A線程有三個操作,它們在程序中的順序是:A1->A2->A3。B線程也有三個操作,它們在程序中的順序是:B1->B2->B3。
假設這兩個線程使用監視器來正確同步:A線程的三個操作執行后釋放監視器,隨后B線程獲取同一個監視器。那么程序在順序一致性模型中的執行效果將如下圖所示:
假設這兩個線程沒有做同步,下面是這個未同步程序在順序一致性模型中的執行示意圖:
未同步程序在順序一致性模型中雖然整體執行順序是無序的,但所有線程都只能看到一個一致的整體執行順序。以上圖為例,線程A和B看到的執行順序都是:B1->A1->A2->B2->A3->B3。之所以能得到這個保證是因為順序一致性內存模型中的每個操作必須立即對任意線程可見。
但是,在JMM中就沒有這個保證。未同步程序在JMM中不但整體的執行順序是無序的,而且所有線程看到的操作執行順序也可能不一致。比如,在當前線程把寫過的數據緩存在本地內存中,且還沒有刷新到主內存之前,這個寫操作僅對當前線程可見;從其他線程的角度來觀察,會認為這個寫操作根本還沒有被當前線程執行。只有當前線程把本地內存中寫過的數據刷新到主內存之后,這個寫操作才能對其他線程可見。在這種情況下,當前線程和其它線程看到的操作執行順序將不一致。
3.3 同步程序的執行特性
【例】
class SynchronizedExample { int a = 0; boolean flag = false; public synchronized void writer() { a = 1; flag = true; } public synchronized void reader() { if (flag) { int i = a; …… } } }
在順序一致性模型中,所有操作完全按程序的順序串行執行。而在JMM中,臨界區內的代碼可以重排序。
3.4 未同步程序的執行特性
對于未同步或未正確同步的多線程程序,JMM只提供最小安全性:線程執行時讀取到的值,要么是之前某個線程寫入的值,要么是默認值(0,null,false),JMM保證線程讀操作讀取到的值不會無中生有(out of thin air)的冒出來。
為了實現最小安全性,JVM在堆上分配對象時,首先會清零內存空間,然后才會在上面分配對象(JVM內部會同步這兩個操作)。因此,在以清零的內存空間(pre-zeroed memory)分配對象時,域的默認初始化已經完成了。
JMM不保證未同步程序的執行結果與該程序在順序一致性模型中的執行結果一致。因為未同步程序在順序一致性模型中執行時,整體上是無序的,其執行結果無法預知。保證未同步程序在兩個模型中的執行結果一致毫無意義。
和順序一致性模型一樣,未同步程序在JMM中的執行時,整體上也是無序的,其執行結果也無法預知。同時,未同步程序在這兩個模型中的執行特性有下面幾個差異:
- 順序一致性模型保證單線程內的操作會按程序的順序執行,而JMM不保證單線程內的操作會按程序的順序執行(比如上面正確同步的多線程程序在臨界區內的重排序)?!拔囊咽?
- 順序一致性模型保證所有線程只能看到一致的操作執行順序,而JMM不保證所有線程能看到一致的操作執行順序?!拔囊咽?
- JMM不保證對64位的long型和double型變量的讀/寫操作具有原子性,而順序一致性模型保證對所有的內存讀/寫操作都具有原子性。
關于第三點:
第三點差異與處理器總線的工作機制密切相關。在計算機中,數據通過總線在處理器和內存之間傳遞。每次處理器和內存之間的數據傳遞都是通過一系列步驟來完成的,這一系列步驟稱之為總線事務(bus transaction)??偩€事務包括讀事務(read transaction)和寫事務(write transaction)。讀事務從內存傳送數據到處理器,寫事務從處理器傳送數據到內存,每個事務會讀/寫內存中一個或多個物理上連續的字。這里的關鍵是,總線會同步試圖并發使用總線的事務。在一個處理器執行總線事務期間,總線會禁止其它所有的處理器和I/O設備執行內存的讀/寫。
在一些32位的處理器上,如果要求對64位數據的讀/寫操作具有原子性,會有比較大的開銷。為了照顧這種處理器,java語言規范鼓勵但不強求JVM對64位的long型變量和double型變量的讀/寫具有原子性。當JVM在這種處理器上運行時,會把一個64位long/ double型變量的讀/寫操作拆分為兩個32位的讀/寫操作來執行。這兩個32位的讀/寫操作可能會被分配到不同的總線事務中執行,此時對這個64位變量的讀/寫將不具有原子性。
當單個內存操作不具有原子性,將可能會產生意想不到后果。請看下面示意圖:
如上圖所示,假設處理器A寫一個long型變量,同時處理器B要讀這個long型變量。處理器A中64位的寫操作被拆分為兩個32位的寫操作,且這兩個32位的寫操作被分配到不同的寫事務中執行。同時處理器B中64位的讀操作被拆分為兩個32位的讀操作,且這兩個32位的讀操作被分配到同一個的讀事務中執行。當處理器A和B按上圖的時序來執行時,處理器B將看到僅僅被處理器A“寫了一半“的無效值。
4、volatile
把對volatile變量的單個讀/寫,看成是使用同一個監視器鎖對這些單個讀/寫操作做了同步。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入。
這意味著即使是64位的long型和double型變量,只要它是volatile變量,對該變量的讀寫就將具有原子性。如果是多個volatile操作或類似于volatile++這種復合操作,這些操作整體上不具有原子性。
簡而言之,volatile變量自身具有下列特性:
- 可見性。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入。
- 原子性:對任意單個volatile變量的讀/寫具有原子性,但類似于volatile++這種復合操作不具有原子性。
4.1 volatile寫-讀建立的happens before關系
從JSR-133開始,volatile變量的寫-讀可以實現線程之間的通信。
從內存語義的角度來說,volatile與監視器鎖有相同的效果:volatile寫和監視器的釋放有相同的內存語義;volatile讀與監視器的獲取有相同的內存語義。
class VolatileExample { int a = 0; volatile boolean flag = false; public void writer() { a = 1; //1 flag = true; //2 } public void reader() { if (flag) { //3 int i = a; //4 …… } } }
假設線程A執行writer()方法之后,線程B執行reader()方法。根據happens before規則,這個過程建立的happens before 關系可以分為兩類:
- 根據程序次序規則,1 happens before 2; 3 happens before 4。
- 根據volatile規則,2 happens before 3。
- 根據happens before 的傳遞性規則,1 happens before 4。
上圖中,每一個箭頭鏈接的兩個節點,代表了一個happens before 關系。黑色箭頭表示程序順序規則;橙色箭頭表示volatile規則;藍色箭頭表示組合這些規則后提供的happens before保證。
這里A線程寫一個volatile變量后,B線程讀同一個volatile變量。A線程在寫volatile變量之前所有可見的共享變量,在B線程讀同一個volatile變量后,將立即變得對B線程可見。
4.2 volatile寫-讀的內存語義
volatile寫的內存語義如下:
- 當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存。
以上面示例程序VolatileExample為例,假設線程A首先執行writer()方法,隨后線程B執行reader()方法,初始時兩個線程的本地內存中的flag和a都是初始狀態。
下圖是線程A執行volatile寫后,共享變量的狀態示意圖。線程A在寫flag變量后,本地內存A中被線程A更新過的兩個共享變量的值被刷新到主內存中。此時,本地內存A和主內存中的共享變量的值是一致的。
volatile讀的內存語義如下:
- 當讀一個volatile變量時,JMM會把該線程對應的本地內存置為無效。線程接下來將從主內存中讀取共享變量。
下面是線程B讀同一個volatile變量后,共享變量的狀態示意圖。在讀flag變量后,本地內存B已經被置為無效。此時,線程B必須從主內存中讀取共享變量。線程B的讀取操作將導致本地內存B與主內存中的共享變量的值也變成一致的了。
把volatile寫和volatile讀這兩個步驟綜合起來看的話,在讀線程B讀一個volatile變量后,寫線程A在寫這個volatile變量之前所有可見的共享變量的值都將立即變得對讀線程B可見。
下面對volatile寫和volatile讀的內存語義做個總結:
- 線程A寫一個volatile變量,實質上是線程A向接下來將要讀這個volatile變量的某個線程發出了(其對共享變量所在修改的)消息。
- 線程B讀一個volatile變量,實質上是線程B接收了之前某個線程發出的(在寫這個volatile變量之前對共享變量所做修改的)消息。
- 線程A寫一個volatile變量,隨后線程B讀這個volatile變量,這個過程實質上是線程A通過主內存向線程B發送消息。
4.3 volatile內存語義的實現
為了實現volatile內存語義,JMM會分別限制編譯器重排序和處理器重排序。下面是JMM針對編譯器制定的volatile重排序規則表:
是否能重排序 | 第二個操作 | ||
第一個操作 | 普通讀/寫 | volatile讀 | volatile寫 |
普通讀/寫 | NO | ||
volatile讀 | NO | NO | NO |
volatile寫 | NO | NO |
舉例來說,第三行最后一個單元格的意思是:在程序順序中,當第一個操作為普通變量的讀或寫時,如果第二個操作為volatile寫,則編譯器不能重排序這兩個操作。
從上表我們可以看出:
- 當第二個操作是volatile寫時,不管第一個操作是什么,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之后。
- 當第一個操作是volatile讀時,不管第二個操作是什么,都不能重排序。這個規則確保volatile讀之后的操作不會被編譯器重排序到volatile讀之前。
- 當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。
轉自http://blog.csdn.net/vking_wang/article/details/8574376
原創文章,作者:stanley,如若轉載,請注明出處:http://www.www58058.com/1052