線程之間的通訊機制有兩種:共享內存和消息傳遞;在共享內存的併發模型裏,線程之間共享程序的公共狀態,線程之間經過寫-讀內存中的公共狀態來隱式進行通訊。在消息傳遞的併發模型裏,線程之間沒有公共狀態,線程之間必須經過明確的發送消息來顯式進行通訊。
同步是指程序用於控制不一樣線程之間操做發生相對順序的機制。在共享內存併發模型裏,同步是顯式進行的。工程師必須顯式指定某個方法或某段代碼須要在線程之間互斥執行。在消息傳遞的併發模型裏,因爲消息的發送必須在消息的接收以前,所以同步是隱式進行的。
Java的併發採用的是共享內存模型,Java線程之間的通訊老是隱式進行,整個通訊過程對工程師徹底透明。java
2.Java內存模型的抽象
在java中,全部實例域、靜態域和數組元素存儲在堆內存中,堆內存在線程之間共享(本文使用「共享變量」這個術語代指實例域,靜態域和數組元素)。局部變量,方法定義參數和異常處理器參數不會在線程之間共享,它們不會有內存可見性問題,也不受內存模型的影響。
Java線程之間的通訊由Java內存模型(本文簡稱爲JMM)控制,JMM決定一個線程對共享變量的寫入什麼時候對另外一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存中,每一個線程都有一個私有的本地內存,本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其餘的硬件和編譯器優化。Java內存模型的抽象示意圖以下:程序員
從上圖來看,線程A與線程B之間如要通訊的話,必需要經歷下面2個步驟:編程
3.從源代碼到指令序列的重排序數組
在執行程序時爲了提升性能,編譯器和處理器經常會對指令作重排序。重排序分三種類型:緩存
從java源代碼到最終實際執行的指令序列,會分別經歷下面三種重排序:多線程
上述的1屬於編譯器重排序,2和3屬於處理器重排序。這些重排序均可能會致使多線程程序出現內存可見性問題。對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(不是全部的編譯器重排序都要禁止)。對於處理器重排序,JMM的處理器重排序規則會要求java編譯器在生成指令序列時,插入特定類型的內存屏障指令,經過內存屏障指令來禁止特定類型的處理器重排序(不是全部的處理器重排序都要禁止)。
JMM屬於語言級的內存模型,它確保在不一樣的編譯器和不一樣的處理器平臺之上,經過禁止特定類型的編譯器重排序和處理器重排序,爲程序員提供一致的內存可見性保證。併發
4.happens-before簡介
happens-before是JMM最核心的概念,對於Java工程師來講,理解happens-before是理解JMM的關鍵。app
JMM的設計意圖編程語言
在設計JMM須要考慮兩個關鍵因素:ide
這兩個因素是互相矛盾的,因此JSR-133專家組設計時須要考慮到一個好的平衡點:一方面爲工程師提供足夠強的內存可見性,另外一方面要對編譯器和處理器的限制要儘可能鬆些。
咱們來舉了例子:
int a=10; //A int b=20; //B int c=a*b; //C 上面是一個簡單的乘法運算,並存在3個happens-before關係: 1. A happens-before B 2. B happens-before C 3. A happens-before C 這三個happens-before關係中,2和3是必須的,但1是沒必要要的。所以,JMM把happens-before要求禁止的重排序分爲兩類: 1.會改變程序執行結果的重排序。 2.不會改變程序執行結果的重排序。 JMM對這兩種不一樣性質的重排序,採起了不一樣的策略: 1.對於會改變程序執行結果的重排序,JMM要求編譯器和處理器必須禁止這種重排序。 2.對於不會改變程序執行結果的重排序,JMM要求編譯器和處理器不作要求,能夠容許這種重排序。
happens-before的定義與規則
JSR-133使用happens-before的概念來指定兩個操做之間的執行順序,因爲這兩個操做能夠在一個線程內,也能夠在不一樣的線程之間。所以,JMM能夠經過happens-before關係向工程師提供跨線程的內存可見性保證。
happens-before規則以下:
1. 程序順序規則:一個線程中的每一個操做,happens- before 於該線程中的任意後續操做。
2. 監視器鎖規則:對一個監視器鎖的解鎖,happens- before 於隨後對這個監視器鎖的加鎖。
3. volatile變量規則:對一個volatile域的寫,happens- before 於任意後續對這個volatile域的讀。
4. 傳遞性:若是A happens- before B,且B happens- before C,那麼A happens- before C。
順序一致性內存模型是一個理論參考模型,在設計的時候,處理器的內存模型和編程語言的內存模型都會以順序一致性內存模型爲參考。
當程序未正確同步時,就會存在數據競爭。數據競爭指的是:在一個線程中寫一個變量,在另外一個線程讀同一個變量,並且寫和讀沒有經過同步來排序。
當代碼中包含數據競爭時,程序的執行每每產生違反直覺的結果。若是一個多線程程序能正確同步,這個程序將是一個沒有數據競爭的程序。
JMM對正確同步的多線程程序的內存一致性作了以下保證:
若是程序是正確同步的,程序的執行將具備順序一致性(sequentially consistent),即程序的執行結果與該程序在順序一致性內存模型中的執行結果相同。這裏的同步是指廣義上的同步,包括對經常使用同步原語(synchronized,volatile和final)的正確使用。
順序一致性內存模型是一個被計算機科學家理想化了的理論參考模型,它爲程序員提供了極強的內存可見性保證。順序一致性內存模型有兩大特性:
順序一致性內存模型爲程序員提供的視圖以下:
在概念上,順序一致性模型有一個單一的全局內存,這個內存經過一個左右擺動的開關能夠鏈接到任意一個線程。同時,每個線程必須按程序的順序來執行內存讀/寫操做。從上圖咱們能夠看出,在任意時間點最多隻能有一個線程能夠鏈接到內存。當多個線程併發執行時,圖中的開關裝置能把全部線程的全部內存讀/寫操做串行化。
順序一致性內存模型中的每一個操做必須當即對任意線程可見,可是在JMM中就沒有這個保證。未同步程序在JMM中不但總體的執行順序是無序的,並且全部線程看到的操做執行順序也可能不一致。好比,在當前線程把寫過的數據緩存在本地內存中,且尚未刷新到主內存以前,這個寫操做僅對當前線程可見;從其餘線程的角度來觀察,會認爲這個寫操做根本尚未被當前線程執行。只有當前線程把本地內存中寫過的數據刷新到主內存以後,這個寫操做才能對其餘線程可見。在這種狀況下,當前線程和其它線程看到的操做執行順序將不一致。
咱們接下來看看正確同步的程序如何具備順序一致性。
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; …… } } }
上面示例代碼中,假設A線程執行writer()方法後,B線程執行reader()方法。這是一個正確同步的多線程程序。根據JMM規範,該程序的執行結果將與該程序在順序一致性模型中的執行結果相同。下面是該程序在兩個內存模型中的執行時序對比圖:
在順序一致性模型中,全部操做徹底按程序的順序串行執行。而在JMM中,臨界區內的代碼能夠重排序(但JMM不容許臨界區內的代碼「逸出」到臨界區以外,那樣會破壞監視器的語義)。JMM會在退出監視器和進入監視器這兩個關鍵時間點作一些特別處理,使得線程在這兩個時間點具備與順序一致性模型相同的內存視圖。雖然線程A在臨界區內作了重排序,但因爲監視器的互斥執行的特性,這裏的線程B根本沒法「觀察」到線程A在臨界區內的重排序。這種重排序既提升了執行效率,又沒有改變程序的執行結果。
從這裏咱們能夠看到JMM在具體實現上的基本方針:在不改變(正確同步的)程序執行結果的前提下,儘量的爲編譯器和處理器的優化打開方便之門。
JMM不保證未同步程序的執行結果與該程序在順序一致性模型中的執行結果一致。由於未同步程序在順序一致性模型中執行時,總體上是無序的,其執行結果沒法預知。保證未同步程序在兩個模型中的執行結果一致毫無心義。
和順序一致性模型同樣,未同步程序在JMM中的執行時,總體上也是無序的,其執行結果也沒法預知。
同時,未同步程序在這兩個模型中的執行特性有下面幾個差別:
對於第三個差別:在一些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「寫了一半「的無效值。