一篇文章看懂Java併發和執行緒安全(二)

tianxiaoxu發表於2018-07-09

【本文轉自爪哇筆記 作者:冷血狂魔  原文連結:https://mp.weixin.qq.com/s/QDioJjwBC6GImGkfYLS7rw】
前言

上一篇博文《一篇文章看懂Java併發和執行緒安全(一)》講述了多執行緒中,程式總不能按照我們所看到的那樣執行,必須保證共享資料的可見性和執行臨界區程式碼的有序性,才能讓多執行緒程式執行成我們想要的樣子,本篇部落格將繼續深入講解一個有序而又亂序的Java世界。

本部落格重點導讀

工作記憶體與主記憶體的資料交換的細節

指令重排序與記憶體屏障

volatile、final、鎖的記憶體語義

as-if-serial、happens-before

進入細節

主記憶體與工作記憶體互動協議

JMM定義了8種基本操作來完成,主記憶體、工作記憶體和執行引擎之間的互動,分別是lock、unlock、read、load、use、assign、store、write,虛擬機器的實現向程式設計師保證每一種操作都是原子的,不可分割,對於double和long型別的64為變數不做保證。瞭解了這些,有助於幫我們理解記憶體屏障。

別看有8個操作,實際上是成對定義的連貫操作。我們具體來看怎麼記憶。

針對於主記憶體的單獨操作lock和unlock

·lock:作用於主記憶體、把變數標示為執行緒獨佔

·unlock:作用於主記憶體、釋放鎖定狀態

主記憶體到工作記憶體的讀交換

·read:作用於主記憶體,把主記憶體變數傳遞給工作記憶體

·load:作用於工作記憶體,把read操作傳過來的值放入工作記憶體

工作記憶體到主記憶體的寫交換

·store:作用於工作記憶體,把工作記憶體變數傳遞給主記憶體

·write:作用於主記憶體,把store過來的值寫入主記憶體變數

工作記憶體和執行引擎的資料交換

·use:作用於工作記憶體,把工作記憶體變數傳遞給執行引擎

·assign:作用於工作記憶體,把執行引擎的值賦給工作記憶體變數

上述的互動關係,可以用如下的圖來表示:

一篇文章看懂Java併發和執行緒安全(二)

總體來說,工作記憶體和主記憶體的資料交換讀寫都是用兩組操作來完成,而執行引擎和工作記憶體的資料交換由兩個操作完成。當然,上述的8種操作必須滿足一些規則,這裡列舉一些我認為重要的,例如:

·read和load、store和write必須同時出現

·對變數執行lock操作,會清空工作記憶體中快取的該值,對變數執行unlock操作,必須先把值同步回主記憶體。

廢了這麼大的篇幅,講我們Java程式設計師並不關心的資料交換細節,是為了幫助我們理解後面的記憶體屏障,繫好安全帶,我們繼續來看一個完全錯亂的Java微觀世界。

亂序的Java世界

在單執行緒的世界裡,JMM向我們保證執行的正確性,那麼我們可以邏輯的認為程式碼是根據我們編寫的順序執行。那麼在多執行緒的世界裡,站一個執行緒的視角看另一個執行緒,我們將完全看不清執行的順序。並且也看不到對方執行結果。請看下面的程式碼:

一篇文章看懂Java併發和執行緒安全(二)

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

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

一篇文章看懂Java併發和執行緒安全(二)

站在B執行緒的視角,B執行緒中read方法裡的程式碼是否會重排序呢,雖然這個方法的兩句話存在依賴關係,JMM支援不改變結果的指令重排,JMM無法預先判斷是否有其他執行緒在修改a的值,所以可能會重排,並且處理器會用猜測執行來重排。請看下錶:

一篇文章看懂Java併發和執行緒安全(二)

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

記憶體屏障

指令重排序會導致多執行緒執行的無序,那麼JMM會禁止特定型別的指令重排序,JMM透過記憶體屏障來禁止某些指令重排序,那麼有哪些記憶體屏障呢?總共4類

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

·StoreStore:前面的store會先於後面的store執行,也就是保證記憶體可見性

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

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

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

volatile

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

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

final

final本質上定義是final域與構造物件的引用之間的記憶體屏障。

在建構函式對final變數的寫人,與對建構函式物件引用的讀,不能重排序,本質上是插入了storeStore屏障,保證物件引用被讀之前,已經對final變數進行了寫人。這裡特別注意指標逃逸。

讀含有final變數的物件的引用,與讀final變數不能指令重排序,插入loadload屏障,保證先讀到物件引用,在讀final變數的值,也就是隻要物件構造完成,並且在建構函式中將final值寫入,另外一個執行緒肯定可以讀到,這是JMM的保證。

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關係呢?請看下面:

·一個執行緒內,每個操作happens-before後面的操作

·unlock操作happens-before對這個這個鎖的lock操作

·volatile寫操作happens-before讀操作

·執行緒的start方法happens-before此執行緒的所有其他操作

·執行緒所有操作happens-before對此執行緒的終止監測,例如,A執行緒呼叫B執行緒的join方法,如果join返回,那麼B執行緒的所有操作必定完成,且B執行緒的所有操作的資料必定對A執行緒可見。

·傳遞性,A happens-before B、B happens-before C,那麼A happens-before C

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

最後還有兩句話

在本執行緒內看,所有的操作都是有序的,這是as-if-serial的保證。

一個執行緒看另一個執行緒,所有的操作都是無序的,主要是兩方面所致,一方面是指令重排序,另一方面是不知道工作記憶體的值什麼時候同步到主記憶體。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31137683/viewspace-2157559/,如需轉載,請註明出處,否則將追究法律責任。

相關文章