這篇文章主要介紹Java中什么是JMM,文中介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們一定要看完!

成都創(chuàng)新互聯(lián)是一家專業(yè)的成都網(wǎng)站建設(shè)公司,我們專注成都網(wǎng)站設(shè)計(jì)、成都網(wǎng)站制作、網(wǎng)絡(luò)營(yíng)銷、企業(yè)網(wǎng)站建設(shè),友情鏈接,廣告投放平臺(tái)為企業(yè)客戶提供一站式建站解決方案,能帶給客戶新的互聯(lián)網(wǎng)理念。從網(wǎng)站結(jié)構(gòu)的規(guī)劃UI設(shè)計(jì)到用戶體驗(yàn)提高,創(chuàng)新互聯(lián)力求做到盡善盡美。
什么是JMM
你要是整這個(gè)我可就不困了。
JMM就是Java內(nèi)存模型(java memory model)。因?yàn)樵诓煌挠布a(chǎn)商和不同的操作系統(tǒng)下,內(nèi)存的訪問有一定的差異,所以會(huì)造成相同的代碼運(yùn)行在不同的系統(tǒng)上會(huì)出現(xiàn)各種問題。所以java內(nèi)存模型(JMM)屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓java程序在各種平臺(tái)下都能達(dá)到一致的并發(fā)效果。
Java內(nèi)存模型規(guī)定所有的變量都存儲(chǔ)在主內(nèi)存中,包括實(shí)例變量,靜態(tài)變量,但是不包括局部變量和方法參數(shù)。每個(gè)線程都有自己的工作內(nèi)存,線程的工作內(nèi)存保存了該線程用到的變量和主內(nèi)存的副本拷貝,線程對(duì)變量的操作都在工作內(nèi)存中進(jìn)行。線程不能直接讀寫主內(nèi)存中的變量。
不同的線程之間也無法訪問對(duì)方工作內(nèi)存中的變量。線程之間變量值的傳遞均需要通過主內(nèi)存來完成。
如果聽起來抽象的話,我可以畫張圖給你看看,會(huì)直觀一點(diǎn):
每個(gè)線程的工作內(nèi)存都是獨(dú)立的,線程操作數(shù)據(jù)只能在工作內(nèi)存中進(jìn)行,然后刷回到主存。這是 Java 內(nèi)存模型定義的線程基本工作方式。
溫馨提醒一下,這里有些人會(huì)把Java內(nèi)存模型誤解為Java內(nèi)存結(jié)構(gòu),然后答到堆,棧,GC垃圾回收,最后和面試官想問的問題相差甚遠(yuǎn)。實(shí)際上一般問到Java內(nèi)存模型都是想問多線程,Java并發(fā)相關(guān)的問題。
這個(gè)簡(jiǎn)單,整個(gè)Java內(nèi)存模型實(shí)際上是圍繞著三個(gè)特征建立起來的。分別是:原子性,可見性,有序性。這三個(gè)特征可謂是整個(gè)Java并發(fā)的基礎(chǔ)。
原子性指的是一個(gè)操作是不可分割,不可中斷的,一個(gè)線程在執(zhí)行時(shí)不會(huì)被其他線程干擾。
面試官拿筆寫了段代碼,下面這幾句代碼能保證原子性嗎?
int i = 2;int j = i; i++; i = i + 1;復(fù)制代碼
第一句是基本類型賦值操作,必定是原子性操作。
第二句先讀取i的值,再賦值到j(luò),兩步操作,不能保證原子性。
第三和第四句其實(shí)是等效的,先讀取i的值,再+1,最后賦值到i,三步操作了,不能保證原子性。
JMM只能保證基本的原子性,如果要保證一個(gè)代碼塊的原子性,提供了monitorenter 和 moniterexit 兩個(gè)字節(jié)碼指令,也就是 synchronized 關(guān)鍵字。因此在 synchronized 塊之間的操作都是原子性的。
可見性指當(dāng)一個(gè)線程修改共享變量的值,其他線程能夠立即知道被修改了。Java是利用volatile關(guān)鍵字來提供可見性的。 當(dāng)變量被volatile修飾時(shí),這個(gè)變量被修改后會(huì)立刻刷新到主內(nèi)存,當(dāng)其它線程需要讀取該變量時(shí),會(huì)去主內(nèi)存中讀取新值。而普通變量則不能保證這一點(diǎn)。
除了volatile關(guān)鍵字之外,final和synchronized也能實(shí)現(xiàn)可見性。
synchronized的原理是,在執(zhí)行完,進(jìn)入unlock之前,必須將共享變量同步到主內(nèi)存中。
final修飾的字段,一旦初始化完成,如果沒有對(duì)象逸出(指對(duì)象為初始化完成就可以被別的線程使用),那么對(duì)于其他線程都是可見的。
在Java中,可以使用synchronized或者volatile保證多線程之間操作的有序性。實(shí)現(xiàn)原理有些區(qū)別:
volatile關(guān)鍵字是使用內(nèi)存屏障達(dá)到禁止指令重排序,以保證有序性。
synchronized的原理是,一個(gè)線程lock之后,必須unlock后,其他線程才可以重新lock,使得被synchronized包住的代碼塊在多線程之間是串行執(zhí)行的。
好的,面試官,內(nèi)存交互操作有8種,我畫張圖給你看吧:
我再補(bǔ)充一下JMM對(duì)8種內(nèi)存交互操作制定的規(guī)則吧:
內(nèi)心:這可以重頭戲呀,可不能出岔子~
很多并發(fā)編程都使用了volatile關(guān)鍵字,主要的作用包括兩點(diǎn):
volatile修飾的變量,當(dāng)一個(gè)線程改變了該變量的值,其他線程是立即可見的。普通變量則需要重新讀取才能獲得最新值。
volatile保證可見性的流程大概就是這個(gè)一個(gè)過程:
先說結(jié)論吧,volatile不能一定能保證線程安全。
怎么證明呢,我們看下面一段代碼的運(yùn)行結(jié)果就知道了:
/**
* @author Ye Hongzhi 公眾號(hào):java技術(shù)愛好者
**/public class VolatileTest extends Thread { private static volatile int count = 0; public static void main(String[] args) throws Exception {
Vector<Thread> threads = new Vector<>(); for (int i = 0; i < 100; i++) {
VolatileTest thread = new VolatileTest();
threads.add(thread);
thread.start();
} //等待子線程全部完成
for (Thread thread : threads) {
thread.join();
} //輸出結(jié)果,正確結(jié)果應(yīng)該是1000,實(shí)際卻是984
System.out.println(count);//984
} @Override
public void run() { for (int i = 0; i < 10; i++) { try { //休眠500毫秒
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
count++;
}
}
}復(fù)制代碼為什么volatile不能保證線程安全?
很簡(jiǎn)單呀,可見性不能保證操作的原子性,前面說過了count++不是原子性操作,會(huì)當(dāng)做三步,先讀取count的值,然后+1,最后賦值回去count變量。需要保證線程安全的話,需要使用synchronized關(guān)鍵字或者lock鎖,給count++這段代碼上鎖:
private static synchronized void add() {
count++;
}復(fù)制代碼首先要講一下as-if-serial語義,不管怎么重排序,(單線程)程序的執(zhí)行結(jié)果不能被改變。
為了使指令更加符合CPU的執(zhí)行特性,最大限度的發(fā)揮機(jī)器的性能,提高程序的執(zhí)行效率,只要程序的最終結(jié)果與它順序化情況的結(jié)果相等,那么指令的執(zhí)行順序可以與代碼邏輯順序不一致,這個(gè)過程就叫做指令的重排序。
重排序的種類分為三種,分別是:編譯器重排序,指令級(jí)并行的重排序,內(nèi)存系統(tǒng)重排序。整個(gè)過程如下所示:
指令重排序在單線程是沒有問題的,不會(huì)影響執(zhí)行結(jié)果,而且還提高了性能。但是在多線程的環(huán)境下就不能保證一定不會(huì)影響執(zhí)行結(jié)果了。
所以在多線程環(huán)境下,就需要禁止指令重排序。
volatile關(guān)鍵字禁止指令重排序有兩層意思:
當(dāng)程序執(zhí)行到volatile變量的讀操作或者寫操作時(shí),在其前面的操作的更改肯定全部已經(jīng)進(jìn)行,且結(jié)果已經(jīng)對(duì)后面的操作可見,在其后面的操作肯定還沒有進(jìn)行。
在進(jìn)行指令優(yōu)化時(shí),不能將在對(duì)volatile變量訪問的語句放在其后面執(zhí)行,也不能把volatile變量后面的語句放到其前面執(zhí)行。
下面舉個(gè)例子:
private static int a;//非volatile修飾變量private static int b;//非volatile修飾變量private static volatile int k;//volatile修飾變量private void hello() {
a = 1; //語句1
b = 2; //語句2
k = 3; //語句3
a = 4; //語句4
b = 5; //語句5
//以下省略...}復(fù)制代碼變量a,b是非volatile修飾的變量,k則使用volatile修飾。所以語句3不能放在語句1、2前,也不能放在語句4、5后。但是語句1、2的順序是不能保證的,同理,語句4、5也不能保證順序。
并且,執(zhí)行到語句3的時(shí)候,語句1,2是肯定執(zhí)行完畢的,而且語句1,2的執(zhí)行結(jié)果對(duì)于語句3,4,5是可見的。
首先要講一下內(nèi)存屏障,內(nèi)存屏障可以分為以下幾類:
LoadLoad 屏障:對(duì)于這樣的語句Load1,LoadLoad,Load2。在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
StoreStore屏障:對(duì)于這樣的語句Store1, StoreStore, Store2,在Store2及后續(xù)寫入操作執(zhí)行前,保證Store1的寫入操作對(duì)其它處理器可見。
LoadStore 屏障:對(duì)于這樣的語句Load1, LoadStore,Store2,在Store2及后續(xù)寫入操作被刷出前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
StoreLoad 屏障:對(duì)于這樣的語句Store1, StoreLoad,Load2,在Load2及后續(xù)所有讀取操作執(zhí)行前,保證Store1的寫入對(duì)所有處理器可見。
在每個(gè)volatile讀操作后插入LoadLoad屏障,在讀操作后插入LoadStore屏障。
在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障,后面插入一個(gè)SotreLoad屏障。
大概的原理就是這樣。
以上是Java中什么是JMM的所有內(nèi)容,感謝各位的閱讀!希望分享的內(nèi)容對(duì)大家有幫助,更多相關(guān)知識(shí),歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道!
名稱欄目:Java中什么是JMM
URL鏈接:http://www.chinadenli.net/article22/gpdicc.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站建設(shè)、企業(yè)網(wǎng)站制作、關(guān)鍵詞優(yōu)化、建站公司、品牌網(wǎng)站制作、網(wǎng)站策劃
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)