在講JMM之前我們必須先來了解一下現(xiàn)代計算機的工作原理〉值現(xiàn)在的計算機的工作原理叫做馮.諾依曼計算機模型嗡官,結(jié)構(gòu)如下圖:
現(xiàn)代的計算機模型:
CPU的內(nèi)部結(jié)構(gòu)劃分如下:
如上圖所示祟偷,cpu的內(nèi)部結(jié)構(gòu)分為三個部分:控制單元芹橡,運算單元,存儲單元有勾≌钇簦控制單元是整個cpu的指揮中心,由指令寄存器蔼卡、指令譯碼器宏赘、操作控制器組成律杠。根據(jù)程序依次從存儲器中取出各條指令金蜀,放在指令寄存器中,然后指令譯碼器分析確定要進行什么操作茁裙,接著操作控制器對相應(yīng)的部件發(fā)出指令進行操作。運算單元就是根據(jù)控制單元的發(fā)出來的指令進行運算节仿,相當(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在運行某多個程序(進程)的時候,假如只有一個CPU的話模聋,就意味著要經(jīng)常進行進程上下文切換肩民,因為單CPU即便是多核的,也只是多個處理器核心链方,其他設(shè)備都是共用的持痰,所以 多個進程就必然要經(jīng)常進行進程上下文切換,這個代價是很高的祟蚀。?
? 多核是因為比如說現(xià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é)議來進行操作键兜,這類協(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)存中然后再進行操作的。這與硬件的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)存中進行民泵,而不能直接對主存進行操作癣丧。并且每個線程不能訪問其他線程的工作內(nèi)存。Java內(nèi)存模型具備一些先天的“有序性”栈妆,即不需要通過任何手段就能夠得到保證的有序性胁编,這個通常也稱為happens-before 原則。如果兩個操作的執(zhí)行次序無法從happens-before原則推導(dǎo)出來鳞尔,那么它們就不能保證它們的有序性嬉橙,虛擬機可以隨意地對它們進行重排序。
指令重排序:java語言規(guī)范規(guī)定JVM線程內(nèi)部維持順序化語義寥假。即只要程序的最終結(jié)果與它順序化情況的結(jié)果相等市框,那么指令的執(zhí)行順序可以與代碼順序不一致,此過程叫指令的重排序糕韧。指令重排序的意義是什么枫振?JVM能根據(jù)處理器特性(CPU多級緩存系統(tǒng)、多核處理器等)適當(dāng)?shù)膶C器指令進行重排序萤彩,使機器指令能更符合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)存中進行而不能直接讀寫主內(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)存來進行交互乡小。如上圖線程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沒有進入死循環(huán)了吭露,明顯的就知道?thread-0感知到了共享變量的變更》痛椋現(xiàn)在我們解決了緩存數(shù)據(jù)的不一致問題。這就是volatile關(guān)鍵字保證了緩存數(shù)據(jù)的一致性讲竿。但是在高并發(fā)的線程中頻繁的使用volatile就會導(dǎo)致我工作內(nèi)存的數(shù)據(jù)不斷的在外部總線進行數(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)存中進行的,各個線程的工作內(nèi)存是不會相互通信的安券,只能通過主內(nèi)存進行數(shù)據(jù)的通信墩崩。在并發(fā)執(zhí)行的時候就出現(xiàn)了其中一部分線程執(zhí)行完成以后還沒有來得及寫回主存,其他線程就已經(jīng)讀取主內(nèi)存中的未更新的數(shù)據(jù)開始執(zhí)行了侯勉。所以每次都會出現(xiàn)不同的結(jié)果鹦筹,因為線程的原子性就沒有得到保證。
? volatile雖然沒有辦法保證我的原子性址貌,但是可以保證我的有序性铐拐,即我的線程按照代碼順序進行執(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赂摆,寫普通變量
? ? ? ? ? ? ? ? ? ? //分兩步進行挟憔,第一步先volatile讀钟些,第二步再普通寫? ? ? ? ? ? ? ? }
? ? ? ? ? ? });
? ? ? ? ? ? Thread t2 =newThread(new Runnable() {
? ? ? ? ? ? ? ? publicvoid run() {
? ? ? ? ? ? ? ? ? ? b = 1;
? ? ? ? ? ? ? ? ? ? y = a;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? });
? ? ? ? ? ? t1.start();
? ? ? ? ? ? t2.start();
? ? ? ? ? ? t1.join();
? ? ? ? ? ? t2.join();
? ? ? ? ? ? /**? ? ? ? ? ? * cpu或者jit對我們的代碼進行了指令重排烟号?
? ? ? ? ? ? * 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í)行的時候進行了指令重排汪拥,沒有按照程序的執(zhí)行結(jié)果來進行執(zhí)行。先執(zhí)行了x=a,y=b.加上volatile以后就可以解決這個問題篙耗。
?以上就是JMM內(nèi)存模型與volatile,歡迎各位留言評論迫筑,謝謝宪赶。