Java架構(gòu)之路(多線程)JMM和volatile關(guān)鍵字
說到JMM大家一定很陌生,被我們所熟知的一定是jvm虛擬機,而我們今天講的JMM和JVM虛擬機沒有半毛錢關(guān)系,千萬不要把JMM的任何事情聯(lián)想到JVM,把JMM當(dāng)做一個完全新的事物去理解和認(rèn)識。
我們先看一下計算機的理論模型,也是馮諾依曼計算機模型,先來張圖。
其實我們更關(guān)注與計算機的內(nèi)部CPU的計算和內(nèi)存之間的關(guān)系。我們在來深入的看一下是如何計算的。
我們來看一下這個玩意的處理流程啊,當(dāng)我們的數(shù)據(jù)和方法加載的內(nèi)存區(qū),需要處理時,內(nèi)存將數(shù)據(jù)和方法傳遞到CPU的L3->L2->L1然后再進(jìn)入到CPU進(jìn)行計算,然后再由L1->L2->L3->再返回到主內(nèi)存中,但是我們的科技反展的很快的,現(xiàn)在貌似沒有單核的CPU了吧,什么8核16核的CPU隨處可見,我們這里只的CPU只是CPU的一個核來計算這些玩意。假設(shè)我們的方法是f(x) = x + 1,我們?nèi)雲(yún)⑹?,期望得到結(jié)果是2,1+1=2,我計算的沒錯吧。如果我們兩個核同時執(zhí)行該方法呢?我們的CPU2反應(yīng)稍微慢了一點呢?假如當(dāng)我們的內(nèi)存再向CPU2發(fā)送參數(shù)時,可能CPU1已經(jīng)計算完成并且已經(jīng)返回了。這時CPU2取得的參數(shù)就是2,這時再進(jìn)行計算就是2+1了。結(jié)果并不是我們期望的結(jié)果。這其實就是數(shù)據(jù)未同步造成的。我們應(yīng)該想盡我們的辦法去同步一下數(shù)據(jù)。
我們中間加了一層緩存一致性協(xié)議。也就是我們的MESI,在多處理器系統(tǒng)中,每個處理器都有自己的高速緩存,而它們的又共享同一個主內(nèi)存
我來簡單說一下,我們的MESI是咋回事,是怎么做到緩存一致性的。英語不好,我就不誤解大家解釋MESI是什么單詞的縮寫了(是不是縮寫我也不知道,但是我知道工作原理)。
我們還是從內(nèi)存到CPU的這條線路,這時我們多了一個MESI,當(dāng)變量X被共同讀取時,CPU1和CPU2是共享一個X變量,但是分別存在CPU1和2內(nèi),也就是我們X(S)的狀態(tài)。然后CPU1和2一起準(zhǔn)備要計算了。
然后1和2一定會有一個厲害的。比如1得到了勝利,這時CPU1里的X(S)變?yōu)閄(E)由共享狀態(tài)變?yōu)楠毾頎顟B(tài),并且告訴CPU2把X(S)變?yōu)閄(I)的狀態(tài),由共享狀態(tài)變?yōu)槭顟B(tài)。然后CPU1就可以計算了。計算完成,
又將X(S)變?yōu)閄(M)的狀態(tài),由獨享狀態(tài)變?yōu)榱诵薷牡臓顟B(tài)。
M: 被修改(Modified)
該緩存行只被緩存在該CPU的緩存中,并且是被修改過的(dirty),即與主存中的數(shù)據(jù)不一致,該緩存行中的內(nèi)存需要在未來的某個時間點(允許其它CPU讀取請主存中相應(yīng)內(nèi)存之前)寫回(write back)主存。
當(dāng)被寫回主存之后,該緩存行的狀態(tài)會變成獨享(exclusive)狀態(tài)。
E: 獨享的(Exclusive)
該緩存行只被緩存在該CPU的緩存中,它是未被修改過的(clean),與主存中數(shù)據(jù)一致。該狀態(tài)可以在任何時刻當(dāng)有其它CPU讀取該內(nèi)存時變成共享狀態(tài)(shared)。
同樣地,當(dāng)CPU修改該緩存行中內(nèi)容時,該狀態(tài)可以變成Modified狀態(tài)。
S: 共享的(Shared)
該狀態(tài)意味著該緩存行可能被多個CPU緩存,并且各個緩存中的數(shù)據(jù)與主存數(shù)據(jù)一致(clean),當(dāng)有一個CPU修改該緩存行中,其它CPU中該緩存行可以被作廢(變成無效狀態(tài)(Invalid))。
I: 無效的(Invalid)
說到這也就是是我們JMM的內(nèi)存模型的工作機制了。所以說JMM是一個虛擬的,和JVM一點關(guān)系都沒有的。切記不要混淆。
這里也有三個重要的知識點。
JVM 內(nèi)存模型(JMM) 三大特性
原子性:指一個操作是不可中斷的,即使是多個線程一起執(zhí)行的時候,一個操作一旦開始,就不會被其他線程干擾
比如,對于一個靜態(tài)全局變量int i,兩個線程同時對它賦值,線程A 給他賦值 1,線程 B 給它賦值為 -1,。那么不管這兩個線程以何種方式,何種步調(diào)工作,i的值要么是1,要么是-1,線程A和線程B之間是沒有干擾的。這就是原子性的一個特點,不可被中斷
可見性:指當(dāng)一個線程修改了某一個共享變量的值,其他線程是否能夠立即知道這個修改。顯然,對于串行程序來說,可見性問題 是不存在。因為你在任何一個操作步驟中修改某個變量,那么在后續(xù)的步驟中,讀取這個變量的值,一定是修改后的新值。但是這個問題在并行程序中就不見得了。如果一個線程修改了某一個全局變量,那么其他線程未必可以馬上知道這個改動。
有序性:對于一個線程的執(zhí)行代碼而言,我們總是習(xí)慣地認(rèn)為代碼的執(zhí)行時從先往后,依次執(zhí)行的。這樣的理解也不能說完全錯誤,因為就一個線程而言,確實會這樣。但是在并發(fā)時,程序的執(zhí)行可能就會出現(xiàn)亂序。給人直觀的感覺就是:寫在前面的代碼,會在后面執(zhí)行。有序性問題的原因是因為程序在執(zhí)行時,可能會進(jìn)行指令重排,重排后的指令與原指令的順序未必一致(指令重排后面會說)。
我們來看一下volatile關(guān)鍵字
先看一段代碼吧,不上代碼,總覺得是自己沒練習(xí)到位。
- private static int counter = 0;
- public static void main(String[] args) {
- for (int i = 0; i < 10; i++) {
- Thread thread = new Thread(()->{
- for (int j = 0; j < 1000; j++) {
- counter++; //不是一個原子操作,第一輪循環(huán)結(jié)果是沒有刷入主存,這一輪循環(huán)已經(jīng)無效
- }
- });
- thread.start();
- }
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(counter);
- }
按照J(rèn)MM的思想流程來解讀一下這段代碼,我們先創(chuàng)建10個線程。我們這里叫做T1,T2,T3...T100。然后分別去拿counter這個數(shù)字,然后疊加1,循環(huán)1000-counter次。當(dāng)T1拿到counter,開始計算,假如,我們計算到第50次時,這時線程T2,也開始要拿counter這個數(shù)字,這時得到的counter數(shù)字為50,則T2就要循環(huán)950次,最后我們計算得到的counter就是9950。也就是說,內(nèi)部是沒有內(nèi)存一致性協(xié)議的。所以我們的輸出一定是<=10000的數(shù)字。
我們來嘗試改一下代碼,使用一下我們的volatile關(guān)鍵字。
- private static volatile int counter = 0;
- public static void main(String[] args) {
- for (int i = 0; i < 10; i++) {
- Thread thread = new Thread(()->{
- for (int j = 0; j < 1000; j++) {
- counter++; //不是一個原子操作,第一輪循環(huán)結(jié)果是沒有刷入主存,這一輪循環(huán)已經(jīng)無效
- }
- });
- thread.start();
- }
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(counter);
- }
這時我們加入了volatile關(guān)鍵字,我們經(jīng)過多次運行會發(fā)現(xiàn),每次結(jié)果都為10000,也就是說每次都是我們期待的結(jié)果,volatile可以保證線程可見性且提供了一定的有序性,但是無法保證原子性。在JVM底層volatile是采用“內(nèi)存屏障”來實現(xiàn)的。
也就是我們加入了volatile關(guān)鍵字時,java代碼運行過程中,會強制給予一層內(nèi)存一致性的屏障,做到了,我們計算直接不會相互影響,得到我們預(yù)期的結(jié)果。
1、可見性實現(xiàn):
在前文中已經(jīng)提及過,線程本身并不直接與主內(nèi)存進(jìn)行數(shù)據(jù)的交互,而是通過線程的工作內(nèi)存來完成相應(yīng)的操作。這也是導(dǎo)致線程間數(shù)據(jù)不可見的本質(zhì)原因。因此要實現(xiàn)volatile變量的可見性,直接從這方面入手即可。對volatile變量的寫操作與普通變量的主要區(qū)別有兩點:
?。?)修改volatile變量時會強制將修改后的值刷新的主內(nèi)存中。
?。?)修改volatile變量后會導(dǎo)致其他線程工作內(nèi)存中對應(yīng)的變量值失效。因此,再讀取該變量值的時候就需要重新從讀取主內(nèi)存中的值。相當(dāng)于上文說到的從S->E,另一個線程從S->I的過程。
通過這兩個操作,就可以解決volatile變量的可見性問題。
2、內(nèi)存屏障
為了實現(xiàn)volatile可見性和happen-befor的語義。JVM底層是通過一個叫做“內(nèi)存屏障”的東西來完成。內(nèi)存屏障,也叫做內(nèi)存柵欄,是一組處理器指令,用于實現(xiàn)對內(nèi)存操作的順序限制。下面是完成上述規(guī)則所要求的內(nèi)存屏障:
Required barriers | 2nd operation | |||
1st operation | Normal Load | Normal Store | Volatile Load | Volatile Store |
Normal Load | LoadStore | |||
Normal Store | StoreStore | |||
Volatile Load | LoadLoad | LoadStore | LoadLoad | LoadStore |
Volatile Store | StoreLoad | StoreStore |
執(zhí)行順序:Load1—>Loadload—>Load2
確保Load2及后續(xù)Load指令加載數(shù)據(jù)之前能訪問到Load1加載的數(shù)據(jù)。
(2)StoreStore 屏障
執(zhí)行順序:Store1—>StoreStore—>Store2
確保Store2以及后續(xù)Store指令執(zhí)行前,Store1操作的數(shù)據(jù)對其它處理器可見。
(3)LoadStore 屏障
執(zhí)行順序:Load1—>LoadStore—>Store2
確保Store2和后續(xù)Store指令執(zhí)行前,可以訪問到Load1加載的數(shù)據(jù)。
(4)StoreLoad 屏障
執(zhí)行順序: Store1—> StoreLoad—>Load2
確保Load2和后續(xù)的Load指令讀取之前,Store1的數(shù)據(jù)對其他處理器是可見的。
總體上來說volatile的理解還是比較困難的,如果不是特別理解,也不用急,完全理解需要一個過程,在后續(xù)的文章中也還會多次看到volatile的使用場景。這里暫且對volatile的基礎(chǔ)知識和原來有一個基本的了解??傮w來說,volatile是并發(fā)編程中的一種優(yōu)化,在某些場景下可以代替Synchronized。但是,volatile的不能完全取代Synchronized的位置,只有在一些特殊的場景下,才能適用volatile??偟膩碚f,必須同時滿足下面兩個條件才能保證在并發(fā)環(huán)境的線程安全:
(1)對變量的寫操作不依賴于當(dāng)前值。
?。?)該變量沒有包含在具有其他變量的不變式中。
參考地址:https://www.cnblogs.com/paddix/p/5428507.html
JMM-同步八種操作介紹
(1)lock(鎖定):作用于主內(nèi)存的變量,把一個變量標(biāo)記為一條線程獨占狀態(tài)
(2)unlock(解鎖):作用于主內(nèi)存的變量,把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的 變量才可以被其他線程鎖定
(3)read(讀取):作用于主內(nèi)存的變量,把一個變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中, 以便隨后的load動作使用
(4)load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作 內(nèi)存的變量副本中
(5)use(使用):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個變量值傳遞給執(zhí)行引擎
(6)assign(賦值):作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的值賦給工作內(nèi)存 的變量
(7)store(存儲):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個變量的值傳送到主內(nèi)存中, 以便隨后的write的操作
(8)write(寫入):作用于工作內(nèi)存的變量,它把store操作從工作內(nèi)存中的一個變量的值傳送 到主內(nèi)存的變量中
流程圖大致是這樣的: