JAVA內(nèi)存模型JMM解析

JAVA內(nèi)存模型JMM解析

在講JMM之前我們必須先來了解一下現(xiàn)代計算機的工作原理?,F(xiàn)在的計算機的工作原理叫做馮.諾依曼計算機模型,結(jié)構(gòu)如下圖:

現(xiàn)代的計算機模型:


CPU的內(nèi)部結(jié)構(gòu)劃分如下:

如上圖所示,cpu的內(nèi)部結(jié)構(gòu)分為三個部分:控制單元,運算單元,存儲單元??刂茊卧钦麄€cpu的指揮中心,由指令寄存器、指令譯碼器、操作控制器組成。根據(jù)程序依次從存儲器中取出各條指令,放在指令寄存器中,然后指令譯碼器分析確定要進(jìn)行什么操作,接著操作控制器對相應(yīng)的部件發(fā)出指令進(jìn)行操作。運算單元就是根據(jù)控制單元的發(fā)出來的指令進(jìn)行運算,相當(dāng)于執(zhí)行器。存儲單元包括 CPU 片內(nèi)緩存Cache和寄存器組,是 CPU 中暫時存放數(shù)據(jù)的地方,里面保存著那些等待處理的數(shù)據(jù),或已經(jīng)處理過的數(shù)據(jù),CPU 訪問寄存器所用的時間要比訪問內(nèi)存的時間短。 寄存器是CPU內(nèi)部的元件,寄存器擁有非常高的讀寫速度,所以在寄存器之間的數(shù)據(jù)傳送非???。采用寄存器,可以減少 CPU 訪問內(nèi)存的次數(shù),從而提高了 CPU 的工作速度。寄存器組可分為專用寄存器和通用寄存器。專用寄存器的作用是固定的,分別寄存相應(yīng)的數(shù)據(jù);而通用寄存器用途廣泛并可由程序員規(guī)定其用途。

計算機有多個cpu的硬件結(jié)構(gòu)如下:

? ?現(xiàn)代計算機基本上都是多核的cpu,這是因為多核的cpu運算速度快。多cpu是因為單cpu在運行某多個程序(進(jìn)程)的時候,假如只有一個CPU的話,就意味著要經(jīng)常進(jìn)行進(jìn)程上下文切換,因為單CPU即便是多核的,也只是多個處理器核心,其他設(shè)備都是共用的,所以 多個進(jìn)程就必然要經(jīng)常進(jìn)行進(jìn)程上下文切換,這個代價是很高的。?

? 多核是因為比如說現(xiàn)在我們要在一臺計算機上跑一個多線程的程序,因為是一個進(jìn)程里的線程,所以需要一些共享一些存儲變量,如果這臺計算機都是單核單線程CPU的話,就意味著這個程序的不同線程需要經(jīng)常在CPU之間的外部總線上通信,同時還要處理不同CPU之間不同緩存導(dǎo)致數(shù)據(jù)不一致的問題,所以在這種場景下多核單CPU的架構(gòu)就能發(fā)揮很大的優(yōu)勢,通信都在內(nèi)部總線,共用同一個緩存。

? CPU寄存器是內(nèi)存的基礎(chǔ),cpu在寄存器上的操作速度遠(yuǎn)大于主內(nèi)存。cpu緩存器是存在于主內(nèi)存與寄存器中間的,是一種容量很小速度很快的存儲器。CPU的速度遠(yuǎn)高于主內(nèi)存,CPU直接從內(nèi)存中存取數(shù)據(jù)要等待一定時間周期,Cache中保存著CPU剛用過或循環(huán)使用的一部分?jǐn)?shù)據(jù),當(dāng)CPU再次使用該部分?jǐn)?shù)據(jù)時可從Cache中直接調(diào)用,減少CPU的等待時間,提高了系統(tǒng)的效率。一個計算機還包含一個主存。所有的CPU都可以訪問主存。主存通常比CPU中的緩存大得多。

多線程環(huán)境下存在的問題

緩存一致性問題

在多處理器系統(tǒng)中,每個處理器都有自己的高速緩存,而它們又共享同一主內(nèi)存(MainMemory)。基于高速緩存的存儲交互很好地解決了處理器與內(nèi)存的速度矛盾,但是也引入了新的問題:緩存一致性(CacheCoherence)。當(dāng)多個處理器的運算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時,將可能導(dǎo)致各自的緩存數(shù)據(jù)不一致的情況,如果真的發(fā)生這種情況,那同步

回到主內(nèi)存時以誰的緩存數(shù)據(jù)為準(zhǔn)呢?為了解決一致性的問題,需要各個處理器訪問緩存時都遵循一些協(xié)議,在讀寫時要根據(jù)協(xié)議來進(jìn)行操作,這類協(xié)議有MSI、

MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等。我們下面來看一個例子

public class MesiTest {

private static boolean iniFlag=false;

public static void testMe(){

System.out.println(Thread.currentThread().getName()+"iniFlag變更測試我開始啦");

iniFlag=true;

System.out.println(Thread.currentThread().getName()+"iniFlag變更測試我結(jié)束啦");

}

public static void main(String[] args) {

new Thread(new Runnable() {

@Override

public void run() {

System.out.println(Thread.currentThread().getName()+"等待測試。。。。。。");

while(!iniFlag){

}

System.out.println(Thread.currentThread().getName()+"測試結(jié)束");

}

}).start();

try {

Thread.sleep(200);

} catch (InterruptedException e) {

e.printStackTrace();

}

new Thread(new Runnable() {

@Override

public void run() {

testMe();

}

}).start();

}

//結(jié)果如下:

Thread-0等待測試。。。。。。

Thread-1iniFlag變更測試我開始啦

Thread-1iniFlag變更測試我結(jié)束啦

//private static boolean iniFlag=false; 變更為private static volatile boolean iniFlag=false; 結(jié)果如下

Thread-0等待測試。。。。。。

Thread-1iniFlag變更測試我開始啦

Thread-1iniFlag變更測試我結(jié)束啦

Thread-0測試結(jié)束

}

根據(jù)以上的第一種結(jié)果來看,說明Thread-0一直在while循環(huán)中,為什么呢。根本原因就是iniFlag的值改變了,但是線程0沒有感知到。這就是上面所說的共享變量緩存數(shù)據(jù)不一致的原因,以及使用緩存一致性協(xié)議的原因所在。下面我們來看一下上面那個程序的運行的過程。在看上面的程序運行的時候我們需要知道java內(nèi)存模型JMM的一個原子操作。先來看看JMM的內(nèi)存模型。

從上圖可以看出是將主內(nèi)存中的共享變量先拷貝到各個線程的工作內(nèi)存中然后再進(jìn)行操作的。這與硬件的CPU結(jié)構(gòu)相似。

Java內(nèi)存模型內(nèi)存交互操作

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)存的變量中

有序性問題

在Java里面,可以通過volatile關(guān)鍵字來保證一定的“有序性”。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程執(zhí)行同步代碼,相當(dāng)于是讓線程順序執(zhí)行同步代碼,自然就保證了有序性。Java內(nèi)存模型:每個線程都有自己的工作內(nèi)存(類似于前面的高速緩存)。線程對變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接對主存進(jìn)行操作。并且每個線程不能訪問其他線程的工作內(nèi)存。Java內(nèi)存模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為happens-before 原則。如果兩個操作的執(zhí)行次序無法從happens-before原則推導(dǎo)出來,那么它們就不能保證它們的有序性,虛擬機可以隨意地對它們進(jìn)行重排序。

指令重排序:java語言規(guī)范規(guī)定JVM線程內(nèi)部維持順序化語義。即只要程序的最終結(jié)果與它順序化情況的結(jié)果相等,那么指令的執(zhí)行順序可以與代碼順序不一致,此過程叫指令的重排序。指令重排序的意義是什么?JVM能根據(jù)處理器特性(CPU多級緩存系統(tǒng)、多核處理器等)適當(dāng)?shù)膶C器指令進(jìn)行重排序,使機器指令能更符合CPU的執(zhí)行特性,最大限度的發(fā)揮機器性能。

as-if-serial語義

as-if-serial語義的意思是:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。

為了遵守as-if-serial語義,編譯器和處理器不會對存在數(shù)據(jù)依賴關(guān)系的操作做重排序,因為這種重排序會改變執(zhí)行結(jié)果。但是,如果操作之間不存在數(shù)據(jù)依賴關(guān)系,這些操作就可能被編譯器和處理器重排序。

happens-before 原則只靠sychronized和volatile關(guān)鍵字來保證原子性、可見性以及有序性,那么編寫并發(fā)程序可能會顯得十分麻煩,幸運的是,從JDK 5開始,Java使用新的JSR-133內(nèi)存模型,提供了happens-before 原則來輔助保證程序執(zhí)行的原子性、可見性以及有序性的問題,它是判斷數(shù)據(jù)是否存在競爭、線程是否安全的依據(jù),happens-before 原則內(nèi)容如下:

1. 程序順序原則,即在一個線程內(nèi)必須保證語義串行性,也就是說按照代碼順序執(zhí)行。

2. 鎖規(guī)則 解鎖(unlock)操作必然發(fā)生在后續(xù)的同一個鎖的加鎖(lock)之前,也就是說,如果對于一個鎖解鎖后,再加鎖,那么加鎖的動作必須在解鎖動作之后(同一個鎖)。

3. volatile規(guī)則 volatile變量的寫,先發(fā)生于讀,這保證了volatile變量的可見性,簡單的理解就是,volatile變量在每次被線程訪問時,都強迫從主內(nèi)存中讀該變量的值,而當(dāng)該變量發(fā)生變化時,又會強迫將最新的值刷新到主內(nèi)存,任何時刻,不同的線程總是能夠看到該變量的最新值。

4. 線程啟動規(guī)則 線程的start()方法先于它的每一個動作,即如果線程A在執(zhí)行線程B的start方法之前修改了共享變量的值,那么當(dāng)線程B執(zhí)行start方法時,線程A對共享變量的修改對線程B可見

5. 傳遞性 A先于B ,B先于C 那么A必然先于C

6. 線程終止規(guī)則 線程的所有操作先于線程的終結(jié),Thread.join()方法的作用是等待當(dāng)前執(zhí)行的線程終止。假設(shè)在線程B終止之前,修改了共享變量,線程A從線程B的join方法成功返回后,線程B對共享變量的修改將對線程A可見。

7. 線程中斷規(guī)則 對線程 interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測線程是否中斷。

8. 對象終結(jié)規(guī)則 對象的構(gòu)造函數(shù)執(zhí)行,結(jié)束先于finalize()方法

volatile內(nèi)存語義

volatile是Java虛擬機提供的輕量級的同步機制。volatile關(guān)鍵字有如下兩個作用保證被volatile修飾的共享變量對所有線程總數(shù)可見的,也就是當(dāng)一個線程修改了一個被volatile修飾共享變量的值,新值總是可以被其他線程立即得知。

禁止指令重排序優(yōu)化。

volatile的可見性

關(guān)于volatile的可見性作用,我們必須意識到被volatile修飾的變量對所有線程總數(shù)立即可

見的,對volatile變量的所有寫操作總是能立刻反應(yīng)到其他線程中;


Java 內(nèi)存模型規(guī)定所有變量都存儲在主內(nèi)存中,每條線程還有自己的工作內(nèi)存,工作內(nèi)存保存了該線程使用到的變量到主內(nèi)存副本拷貝,線程對變量的所有操作(讀取、賦值)都必須在工作內(nèi)存中進(jìn)行而不能直接讀寫主內(nèi)存中的變量,不同線程之間無法相互直接訪問對方工作內(nèi)存中的變量,線程間變量值的傳遞均需要在主內(nèi)存來完成(具體如下圖)。

知道了它的交互操作我們來看看上面的程序在第一次執(zhí)行的過程,如下圖所示:

?從上圖中可以看到,線程0,1剛開始的時候?qū)niFlag=false拷貝到各自的工作內(nèi)存中,這個過程中涉及三步操作:第一步從主內(nèi)存中read 數(shù)據(jù),然后第二步load到工作內(nèi)存中。read 和load操作一定是連續(xù)執(zhí)行的,接下來就是第三步工作內(nèi)存中的程序調(diào)用變量,即use,use完成之后將計算結(jié)果返回到工作內(nèi)存,即第四步assign返回計算結(jié)果,第五步存儲返回結(jié)果到工作內(nèi)存store,接下來工作內(nèi)存需要將計算結(jié)果寫回到主內(nèi)存中這就是第六步write.從上述步驟我們就知道為什么第一次執(zhí)行的時候線程0會在while中死循環(huán)了。因為線程1和2中的變量是各自擁有的是不會相互交互的,只能通過主內(nèi)存來進(jìn)行交互。如上圖線程2將iniFlag=true寫入到主內(nèi)存的時候,我的線程1其實已經(jīng)開始執(zhí)行了,1讀的共享變量是true,你主內(nèi)存的值被修改了之后線程1是不知道的,所以上述程序的第一執(zhí)行結(jié)果顯示Thread-0在while中死循環(huán)。這就是我們所說的緩存變量數(shù)據(jù)不一致。為了解決這個問題我們在總線上使用了緩存一致性協(xié)議(MESI)M 修改 (Modified),E 獨享、互斥 (Exclusive),S 共享 (Shared),I 無效 (Invalid).


在以上程序中我們在給iniFlag前加入了volatile關(guān)鍵字的以后,我們就發(fā)現(xiàn)我們的thread-0沒有進(jìn)入死循環(huán)了,明顯的就知道?thread-0感知到了共享變量的變更?,F(xiàn)在我們解決了緩存數(shù)據(jù)的不一致問題。這就是volatile關(guān)鍵字保證了緩存數(shù)據(jù)的一致性。但是在高并發(fā)的線程中頻繁的使用volatile就會導(dǎo)致我工作內(nèi)存的數(shù)據(jù)不斷的在外部總線進(jìn)行數(shù)據(jù)交互,當(dāng)量達(dá)到一定程度的時候就會導(dǎo)致我外部總線的帶寬被這樣的交互占用,其他的程序無法執(zhí)行,這就是我們所說的總線風(fēng)暴。

?并發(fā)編程的三大特性是:可見性,有序性,原子性。

volatile關(guān)鍵字保證了我們的可見性,但是不保證程序的原子性,代碼如下:

/**?

* <p>Title: Test6.java</p >?

* <p>Description: </p >?

* <p>@datetime 2019年7月11日 上午12:43:17</p >

* <p>$Revision$</p >

* <p>$Date$</p >

* <p>$Id$</p >*/package test1;import java.util.concurrent.locks.AbstractQueuedSynchronizer;import javax.annotation.Resource;/** * @author hong_liping

*

*/publicclass Test6? {

? ? privatestaticvolatileintcount=0;


? ? publicstaticvoid main(String[] args)? {

? ? ? ? for(inti=0;i<10;i++){

? ? ? ? ? ? Thread t1=newThread(new Runnable() {


? ? ? ? ? ? ? ? @Override

? ? ? ? ? ? ? ? publicvoid run() {

? ? ? ? ? ? ? ? ? ? for(intj=0;j<1000;j++){

? ? ? ? ? ? ? ? ? ? ? ? count++;

? ? ? ? ? ? ? ? ? ? }


? ? ? ? ? ? ? ? }

? ? ? ? ? ? });

? ? ? ? ? ? t1.start();

? ? ? ? }

? ? ? ? try {

? ? ? ? ? ? Thread.sleep(20);

? ? ? ? } catch(InterruptedException e) {? ? ? ? ? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? }? ? ? ? System.out.println(count);? ? }}

//執(zhí)行結(jié)果9999,9679

執(zhí)行以上代碼你會發(fā)現(xiàn)每次執(zhí)行結(jié)果都不一樣,我們的理想結(jié)果是1000*10=10000,但是沒有得到我們想要的結(jié)果,這是為啥呢?因為我們每個線程執(zhí)行的時候都是在自己的工作內(nèi)存中進(jìn)行的,各個線程的工作內(nèi)存是不會相互通信的,只能通過主內(nèi)存進(jìn)行數(shù)據(jù)的通信。在并發(fā)執(zhí)行的時候就出現(xiàn)了其中一部分線程執(zhí)行完成以后還沒有來得及寫回主存,其他線程就已經(jīng)讀取主內(nèi)存中的未更新的數(shù)據(jù)開始執(zhí)行了。所以每次都會出現(xiàn)不同的結(jié)果,因為線程的原子性就沒有得到保證。

? volatile雖然沒有辦法保證我的原子性,但是可以保證我的有序性,即我的線程按照代碼順序進(jìn)行執(zhí)行。來看一下下面的代碼:

publicclass OrderTest {

? ? privatestaticintx = 0, y = 0;

? ? privatestaticinta = 0, b =0;

? ? staticObject object =new Object();

? ? publicstaticvoidmain(String[] args)throws InterruptedException {

? ? ? ? inti = 0;

? ? ? ? for (;;){

? ? ? ? ? ? i++;

? ? ? ? ? ? x = 0; y = 0;

? ? ? ? ? ? a = 0; b = 0;

? ? ? ? ? ? Thread t1 =newThread(new Runnable() {

? ? ? ? ? ? ? ? publicvoid run() {

? ? ? ? ? ? ? ? ? ? //由于線程one先啟動,下面這句話讓它等一等線程two. 讀著可根據(jù)自己電腦的實際性能適當(dāng)調(diào)整等待時間.shortWait(10000);

? ? ? ? ? ? ? ? ? ? a = 1;//是讀還是寫?store,volatile寫

? ? ? ? ? ? ? ? ? ? //storeload ,讀寫屏障,不允許volatile寫與第二部volatile讀發(fā)生重排x = b;// 讀還是寫?讀寫都有,先讀volatile,寫普通變量

? ? ? ? ? ? ? ? ? ? //分兩步進(jìn)行,第一步先volatile讀,第二步再普通寫? ? ? ? ? ? ? ? }

? ? ? ? ? ? });

? ? ? ? ? ? Thread t2 =newThread(new Runnable() {

? ? ? ? ? ? ? ? publicvoid run() {

? ? ? ? ? ? ? ? ? ? b = 1;

? ? ? ? ? ? ? ? ? ? y = a;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? });

? ? ? ? ? ? t1.start();

? ? ? ? ? ? t2.start();

? ? ? ? ? ? t1.join();

? ? ? ? ? ? t2.join();

? ? ? ? ? ? /**? ? ? ? ? ? * cpu或者jit對我們的代碼進(jìn)行了指令重排?

? ? ? ? ? ? * 1,1

? ? ? ? ? ? * 0,1

? ? ? ? ? ? * 1,0

? ? ? ? ? ? * 0,0

? ? ? ? ? ? */? ? ? ? ? ? String result = "第" + i + "次 (" + x + "," + y + ")";

? ? ? ? ? ? if(x == 0 && y == 0) {

? ? ? ? ? ? ? ? System.err.println(result);

? ? ? ? ? ? ? ? break;

? ? ? ? ? ? } else {

? ? ? ? ? ? ? ? System.out.println(result);

? ? ? ? ? ? }

? ? ? ? }

? ? }

? ? publicstaticvoidshortWait(long interval){

? ? ? ? longstart = System.nanoTime();

? ? ? ? long end;

? ? ? ? do{

? ? ? ? ? ? end = System.nanoTime();

? ? ? ? }while(start + interval >= end);

? ? }

}

以上當(dāng)x=0,y=0的時候這樣的結(jié)果是怎么出現(xiàn)的呢,這就是因為線程在執(zhí)行的時候進(jìn)行了指令重排,沒有按照程序的執(zhí)行結(jié)果來進(jìn)行執(zhí)行。先執(zhí)行了x=a,y=b.加上volatile以后就可以解決這個問題。

?以上就是JMM內(nèi)存模型與volatile,歡迎各位留言評論,謝謝。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容