一篇文章看懂Java併發和線程安全(二)

1、前言java

   上一篇博客《一篇文章看懂Java併發和線程安全(一)》講述了多線程中,程序總不能按照咱們所看到的那樣執行,必須保證共享數據的可見性和執行臨界區代碼的有序性,才能讓多線程程序運行成咱們想要的樣子,本篇博客將繼續深刻講解一個有序而又亂序的Java世界。程序員

2、本博客重點導讀緩存

    一、工做內存與主內存的數據交換的細節安全

    二、指令重排序與內存屏障多線程

    三、volatile、final、鎖的內存語義併發

    四、as-if-serial、happens-beforeapp

3、進入細節函數

    一、主內存與工做內存交互協議.net

    JMM定義了8種基本操做來完成,主內存、工做內存和執行引擎之間的交互,分別是lock、unlock、read、load、use、assign、store、write,虛擬機的實現向程序員保證每一種操做都是原子的,不可分割,對於double和long類型的64爲變量不作保證。瞭解了這些,有助於幫咱們理解內存屏障。線程

    別看有8個操做,其實是成對定義的連貫操做。咱們具體來看怎麼記憶。

    (1)、針對於主內存的單獨操做lock和unlock

    lock:做用於主內存、把變量標示爲線程獨佔

    unlock:做用於主內存、釋放鎖定狀態

    (2)、主內存到工做內存的讀交換

    read:做用於主內存,把主內存變量傳遞給工做內存

    load:做用於工做內存,把read操做傳過來的值放入工做內存

    (3)、工做內存到主內存的寫交換

    store:做用於工做內存,把工做內存變量傳遞給主內存

    write:做用於主內存,把store過來的值寫入主內存變量

    (4)、工做內存和執行引擎的數據交換

    use:做用於工做內存,把工做內存變量傳遞給執行引擎

    assign:做用於工做內存,把執行引擎的值賦給工做內存變量

   上述的交互關係,能夠用以下的圖來表示:

    

    整體來講,工做內存和主內存的數據交換讀寫都是用兩組操做來完成,而執行引擎和工做內存的數據交換由兩個操做完成。固然,上述的8種操做必須知足一些規則,這裏列舉一些我認爲重要的,例如:

    (1)、read和load、store和write必須同時出現‘

    (2)、對變量執行lock操做,會清空工做內存中緩存的該值,對變量執行unlock操做,必須先把值同步回主內存。

    廢了這麼大的篇幅,講咱們Java程序員並不關心的數據交換細節,是爲了幫助咱們理解後面的內存屏障,繫好安全帶,咱們繼續來看一個徹底錯亂的Java微觀世界。

    二、亂序的Java世界

    在單線程的世界裏,JMM向咱們保證執行的正確性,那麼咱們能夠邏輯的認爲代碼是根據咱們編寫的順序執行。那麼在多線程的世界裏,站一個線程的視角看另外一個線程,咱們將徹底看不清執行的順序。而且也看不到對方執行結果。請看下面的代碼:

public class ReorderTest {
	private int a = 0;
	private int b = 0;
	private int c = 2;

	public void write() {
		a = 1;
		b = 1;
	}

	public void read() {
		if (b == 1) {
			c = a;
		}
	}
}

    假設有兩個線程A、B分別要執行write和read方法,A先進去執行、B隨後執行,先拋開a、b線程可見性問題,假設a、b對線程當即可見。最後c值是多少?多是1,多是2,甚至多是0。接下來具體分析一下爲何。

    站在B的視角看,它看不清a=1和b=1誰先執行,因爲指令重排序,極可能b=1先執行,請看下錶:

    

    站在B線程的視角,B線程中read方法裏的代碼是否會重排序呢,雖然這個方法的兩句話存在依賴關係,JMM支持不改變結果的指令重排,JMM沒法預先判斷是否有其餘線程在修改a的值,因此可能會重排,而且處理器會用猜想執行來重排。請看下錶:

    

    指令重排序讓線程看不清對方線程的執行順序,也就是亂序的,那麼會有哪些級別的指令重排序呢?有三種:編譯器重排序、指令級重排序、內存級重排序。

    三、內存屏障

    指令重排序會致使多線程執行的無序,那麼JMM會禁止特定類型的指令重排序,JMM經過內存屏障來禁止某些指令重排序,那麼有哪些內存屏障呢?總共4類

    LoadLoad:前面的load會先於後面的load裝載

    StoreStore:前面的store會先於後面的store執行,也就是保證內存可見性

    LoadStore:前面的load先於後面的store執行

     StoreLoad:前面的store先於後面的Load執行

    接下來分別看volatile、final、鎖,都有哪些內存語義,加了哪些內存屏障。

    (1)、volatile

    對volatile變量的寫操做,前面插入StoreStore屏障,防止和上面的寫發生重排序;後面插入StoreLoad屏障,防止和後面的讀寫發生重排序。

    對volatile變量的讀操做,後面會插入兩個屏障,分別是LoadLoad、LoadStore,說白了就是,我是volatile變量,無論你下面的變量是讀或者寫,我都要先於你讀。

    (2)、final   

    final本質上定義是final域與構造對象的引用之間的內存屏障。

   在構造函數對final變量的寫人,與對構造函數對象引用的讀,不能重排序,本質上是插入了storeStore屏障,保證對象引用被讀以前,已經對final變量進行了寫人。這裏特別注意指針逃逸。

    讀含有final變量的對象的引用,與讀final變量不能指令重排序,插入loadload屏障,保證先讀到對象引用,在讀final變量的值,也就是隻要對象構造完成,而且在構造函數中將final值寫入,另一個線程確定能夠讀到,這是JMM的保證。

    (3)、鎖

    ReentrantLock中 有個private volatile int state,本質上是用的volatile的內存語義,這裏就省略講了。

    四、as-if-serial、happens-before

    前面說這麼多,指令重排序重排序,弄亂了Java程序,JMM提供volatile、final、鎖來禁止某些指令重排序,那麼記住這些重排序規則並不是簡單的事,JMM用另一種好記的理論來幫助程序員記憶。

    as-if-serial:用通俗的話來解釋一下,單線中,程序邏輯的以咱們看到的順序執行,這裏只是能夠邏輯的認爲順序執行,其實也會有不影響結果的指令重排,例如:

int i=1;
int j=2;
int a=i*j;

    這裏i=1,j=1重排不影響結果,那麼實際上JMM是容許的。  有了as-if-serial,在單線程中,程序員不用擔憂指令重排和內存可見性問題。

    happens-before

    happens-before保證若是A、B兩個操做存在happens before關係,那麼A操做的結果必定對B可見,有了可見性的保證,在加上正確的同步,就能寫出線程安全的代碼。JSR133定義了哪些自然的happens-before關係呢?請看下面:

    (1)、一個線程內,每一個操做happens-before後面的操做

    (2)、unlock操做happens-before對這個這個鎖的lock操做

    (3)、volatile寫操做happens-before讀操做

    (4)、線程的start方法happens-before此線程的全部其餘操做

    (5)、線程全部操做happens-before對此線程的終止監測,例如,A線程調用B線程的join方法,若是join返回,那麼B線程的全部操做一定完成,且B線程的全部操做的數據一定對A線程可見。

    (6)、傳遞性,A happens-before B、B happens-before C,那麼A happens-before C

    最後總結一下,上一篇文章中圍繞可見性和執行臨界區代碼的順序性進行了說明,本篇文章,主要說的是可見性,就本質而言,加內存屏障,就是爲了保證前面的操做對後面的操做可見,也就是我不能和你順序弄亂了,我得看着你怎麼執行,happens-before是JMM對Java程序員的承諾,記住這些規則,配合鎖,一定線程安全。

    最後還有兩句話

    在本線程內看,全部的操做都是有序的,這是as-if-serial的保證。

    一個線程看另外一個線程,全部的操做都是無序的,主要是兩方面所致,一方面是指令重排序,另外一方面是不知道工做內存的值何時同步到主內存。

    

快樂源於分享。

此博客乃做者原創, 轉載請註明出處

相關文章
相關標籤/搜索