前言
樓主這個(gè)標(biāo)題其實(shí)有一種作死的味道昧狮,為什么呢景馁,這三個(gè)東西其實(shí)可以分開為三篇文章來寫,但是逗鸣,樓主認(rèn)為這三個(gè)東西又都是高度相關(guān)的,應(yīng)當(dāng)在一個(gè)知識(shí)點(diǎn)中绰精。在一次學(xué)習(xí)中去理解這些東西撒璧。才能更好的理解 Java 內(nèi)存模型和 volatile 關(guān)鍵字還有 HB 原則。
樓主今天就嘗試著在一篇文章中講述這三個(gè)問題笨使,最后總結(jié)卿樱。
- 講并發(fā)知識(shí)前必須復(fù)習(xí)的硬件知識(shí)。
- Java 內(nèi)存模型到底是什么玩意硫椰?
- Java 內(nèi)存模型定義了哪些東西繁调?
- Java內(nèi)存模型引出的 Happen-Before 原則是什么?
- Happen-Before 引出的 volatile 又是什么靶草?
- 總結(jié)這三者蹄胰。
1. 講并發(fā)知識(shí)前必須復(fù)習(xí)的硬件知識(shí)。
首先奕翔,因?yàn)槲覀冃枰私?Java 虛擬機(jī)的并發(fā)裕寨,而物理硬件的并發(fā)和虛擬機(jī)的并發(fā)很相似,而且虛擬機(jī)的并發(fā)很多看著奇怪的設(shè)計(jì)都是因?yàn)槲锢頇C(jī)的設(shè)計(jì)導(dǎo)致的。
什么是并發(fā)宾袜?多個(gè)CPU同時(shí)執(zhí)行捻艳。但請(qǐng)注意:只有CPU是不行的,CPU 只能計(jì)算數(shù)據(jù)庆猫,那么數(shù)據(jù)從哪里來认轨?
答案:內(nèi)存。 數(shù)據(jù)從內(nèi)存中來月培。需要讀取數(shù)據(jù)嘁字,存儲(chǔ)計(jì)算結(jié)果。有的同學(xué)可能會(huì)說节视,不是有寄存器和多級(jí)緩存嗎拳锚?但是那是靜態(tài)隨機(jī)訪問內(nèi)存(Static Random Access Memory),太貴了寻行,SRAM 在設(shè)計(jì)上使用的晶體管數(shù)量較多霍掺,價(jià)格較高,且不易做成大容量拌蜘,只能用很小的部分集成的CPU中成為CPU的高速緩存杆烁。而正常使用的都是都是動(dòng)態(tài)隨機(jī)訪問內(nèi)存(Dynamic Random Access Memory)。intel 的 CPU 外頻 需要從北橋經(jīng)過訪問內(nèi)存简卧,而AMD 的沒有設(shè)計(jì)北橋兔魂,他與 Intel 不同的地方在于,內(nèi)存是直接與CPU通信而不通過北橋举娩,也就是將內(nèi)存控制組件集成到CPU中析校。理論上這樣可以加速CPU和內(nèi)存的傳輸速度。
好了铜涉,不管哪一家的CPU智玻,都需要從內(nèi)存中讀取數(shù)據(jù),并且自己都有高速緩存或者說寄存器芙代。緩存作什么用呢吊奢?由于CPU的速度很快,內(nèi)存根本跟不上CPU纹烹,因此页滚,需要在內(nèi)存和CPU直接加一層高速緩存讓他們緩沖CPU的數(shù)據(jù):將運(yùn)算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能夠快速執(zhí)行铺呵,當(dāng)運(yùn)算結(jié)束后再?gòu)木彺嫱降絻?nèi)存之中裹驰。這樣處理器就無需等待緩慢的內(nèi)存讀寫了。
但是這樣引出了另一個(gè)問題:緩存一致性(Cache Coherence)陪蜻。什么意思呢邦马?
在多處理器中,每個(gè)處理器都有自己的高速緩存,而他們又共享同一個(gè)主內(nèi)存(Main Memory)滋将,當(dāng)多個(gè)處理器的運(yùn)算任務(wù)都涉及到同一塊主內(nèi)存區(qū)域時(shí)邻悬,將可能導(dǎo)致各自的緩存數(shù)據(jù)不一致。如果真的發(fā)生這種情況随闽,拿同步到主內(nèi)存時(shí)以誰的緩存數(shù)據(jù)為準(zhǔn)呢父丰?
在早期的CPU當(dāng)中,可以通過在總線上加 LOCK# 鎖的形式來解決緩存不一致的問題掘宪。因?yàn)镃PU和其他部件進(jìn)行通信都是通過總線來進(jìn)行的蛾扇,如果對(duì)總線加LOCK#鎖的話,也就是說阻塞了其他CPU對(duì)其他部件訪問(如內(nèi)存)魏滚,從而使得只能有一個(gè)CPU能使用這個(gè)變量的內(nèi)存镀首。
現(xiàn)在的 CPU 為了解決一致性問題,需要各個(gè)CPU訪問(讀或者寫)緩存的時(shí)候遵循一些協(xié)議:MSI鼠次,MESI更哄,MOSI,Synapse腥寇,F(xiàn)irefly成翩,Dragon Protocol,這些都是緩存一致性協(xié)議赦役。
那么麻敌,這個(gè)時(shí)候需要說一個(gè)名詞:內(nèi)存模型。
什么是內(nèi)存模型呢掂摔?
內(nèi)存模型可以理解為在特定的操作協(xié)議下术羔,對(duì)特定的內(nèi)存或高速緩存進(jìn)行讀寫訪問的過程抽象。不同架構(gòu)的CPU 有不同的內(nèi)存模型乙漓,而 Java 虛擬機(jī)屏蔽了不同CPU內(nèi)存模型的差異聂示,這就是Java 的內(nèi)存模型。
那么 Java 的內(nèi)存模型的結(jié)構(gòu)是什么樣子的呢簇秒?
好了,關(guān)于為什么會(huì)有內(nèi)存模型這件事秀鞭,我們已經(jīng)說的差不多了趋观,總體來說就是因?yàn)槎鄠€(gè)CPU的多級(jí)緩存訪問同一個(gè)內(nèi)存條可能會(huì)導(dǎo)致數(shù)據(jù)不一致。所以需要一個(gè)協(xié)議锋边,讓這些處理器在訪問內(nèi)存的時(shí)候遵守這些協(xié)議保證數(shù)據(jù)的一致性皱坛。
還有一個(gè)問題。CPU 的流水線執(zhí)行和亂序執(zhí)行
我們假設(shè)我們現(xiàn)在有一段代碼:
int a = 1;
int b = 2;
int c = a + b;
上面的代碼我們能不能不順序動(dòng)一下并且結(jié)果不變呢豆巨?可以剩辟,第一行和第二行調(diào)換沒有任何問題。
實(shí)際上,CPU 有時(shí)候?yàn)榱藘?yōu)化性能贩猎,也會(huì)對(duì)代碼順序進(jìn)行調(diào)換(在保證結(jié)果的前提下)熊户,專業(yè)術(shù)語叫重排序。為什么重排序會(huì)優(yōu)化性能呢吭服?
這個(gè)就有點(diǎn)復(fù)雜了嚷堡,我們慢慢說。
我們知道艇棕,一條指令的執(zhí)行可以分為很多步驟的蝌戒,簡(jiǎn)單的說,可以分為以下幾步:
- 取指 IF
- 譯碼和取寄存器操作數(shù) ID
- 執(zhí)行或者有效地址計(jì)算 EX
- 存儲(chǔ)器返回 MEM
- 寫回 WB
我們的匯編指令也不是一步就可以執(zhí)行完畢的沼琉,在CPU 中實(shí)際工作時(shí)北苟,他還需要分為多個(gè)步驟依次執(zhí)行,每個(gè)步驟涉及到的硬件也可能不同打瘪,比如友鼻,取指時(shí)會(huì)用到 PC 寄存器和存儲(chǔ)器,譯碼時(shí)會(huì)用到指令寄存器組瑟慈,執(zhí)行時(shí)會(huì)使用 ALU桃移,寫回時(shí)需要寄存器組。
也就是說葛碧,由于每一個(gè)步驟都可能使用不同的硬件完成借杰,因此,CPU 工程師們就發(fā)明了流水線技術(shù)來執(zhí)行指令进泼。什么意思呢蔗衡?
假如你需要洗車,那么洗車店會(huì)執(zhí)行 “洗車” 這個(gè)命令乳绕,但是绞惦,洗車店會(huì)分開操作,比如沖水洋措,打泡沫济蝉,洗刷,擦干菠发,打蠟等王滤,這寫動(dòng)作都可以由不同的員工來做,不需要一個(gè)員工依次取執(zhí)行滓鸠,其余的員工在那干等著予颤,因此诀黍,每個(gè)員工都被分配一個(gè)任務(wù)飞蚓,執(zhí)行完就交給下一個(gè)員工,就像工廠里的流水線一樣曲饱。
CPU 在執(zhí)行指令的時(shí)候也是這么做的。
既然是流水線執(zhí)行珠月,那么流水線肯定不能中斷扩淀,否則,一個(gè)地方中斷會(huì)影響下游所有的組件執(zhí)行效率桥温,性能損失很大引矩。
那么怎么辦呢?打個(gè)比方侵浸,1沖水旺韭,2打泡沫,3洗刷掏觉,4擦干区端,5打蠟 本來是按照順序執(zhí)行的。如果這個(gè)時(shí)候澳腹,水沒有了织盼,那么沖水后面的動(dòng)作都會(huì)收到影響,但是呢酱塔,其實(shí)我們可以讓沖水先去打水沥邻,和打泡沫的換個(gè)位置,這樣羊娃,我們就先打泡沫唐全,沖水的會(huì)在這個(gè)時(shí)候取接水,等到第一輛車的泡沫打完了蕊玷,沖水的就回來了邮利,繼續(xù)趕回,不影響工作垃帅。這個(gè)時(shí)候順序就變成了:
1打泡沫 延届,2沖水,3洗刷贸诚,4擦干方庭,5打蠟.
但是工作絲毫不受影響。流水線也沒有斷酱固。CPU 中的亂序執(zhí)行其實(shí)也跟這個(gè)道理差不多二鳄。其最終的目的,還是為了壓榨 CPU 的性能媒怯。
好了,對(duì)于今天的文章需要的硬件知識(shí)髓窜,我們已經(jīng)復(fù)習(xí)的差不多了扇苞∑鄣睿總結(jié)一下,主要是2點(diǎn):
- CPU 的多級(jí)緩存訪問主存的時(shí)候需要配合緩存一致性協(xié)議鳖敷。這個(gè)過程可以抽象為內(nèi)存模型脖苏。
- CPU 為了性能會(huì)讓指令流水線執(zhí)行,并且會(huì)在單個(gè) CPU 的執(zhí)行結(jié)構(gòu)不混亂的情況下亂序執(zhí)行定踱。
那么棍潘,接下來就要好好說說Java 的內(nèi)存模型了。
2. Java 內(nèi)存模型到底是什么玩意崖媚?
回憶下上面的內(nèi)容亦歉,我們說從硬件的層面什么是內(nèi)存模型?
內(nèi)存模型可以理解為在特定的操作協(xié)議下畅哑,對(duì)特定的內(nèi)存或高速緩存進(jìn)行讀寫訪問的過程抽象肴楷。不同架構(gòu)的CPU 有不同的內(nèi)存模型。
Java 作為跨平臺(tái)語言荠呐,肯定要屏蔽不同CPU內(nèi)存模型的差異赛蔫,構(gòu)造自己的內(nèi)存模型,這就是Java 的內(nèi)存模型泥张。實(shí)際上呵恢,根源來自硬件的內(nèi)存模型。
還是看這個(gè)圖片媚创,Java 的內(nèi)存模型和硬件的內(nèi)存模型幾乎一樣渗钉,每個(gè)線程都有自己的工作內(nèi)存,類似CPU的高速緩存筝野,而 java 的主內(nèi)存相當(dāng)于硬件的內(nèi)存條晌姚。
Java 內(nèi)存模型也是抽象了線程訪問內(nèi)存的過程。
JMM(Java 內(nèi)存模型)規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存(這個(gè)很重要)中歇竟,包括實(shí)例字段挥唠,靜態(tài)字段,和構(gòu)成數(shù)據(jù)對(duì)象的元素焕议,但不包括局部變量和方法參數(shù)宝磨,因?yàn)楹笳呤蔷€程私有的。不會(huì)被共享盅安。自然就沒有競(jìng)爭(zhēng)問題唤锉。
什么是工作內(nèi)存呢?每個(gè)線程都有自己的工作內(nèi)存(這個(gè)很重要)别瞭,線程的工作內(nèi)存保存了該線程使用到的變量和主內(nèi)存副本拷貝窿祥,線程對(duì)變量的所有操作(讀寫)都必須在工作內(nèi)存中進(jìn)行。而不能直接讀寫主內(nèi)存中的變量蝙寨。不同的線程之間也無法訪問對(duì)方工作內(nèi)存中的變量晒衩。線程之間變量值的傳遞均需要通過主內(nèi)存來完成嗤瞎。
總結(jié)一下,Java 內(nèi)存模型定義了兩個(gè)重要的東西听系,1.主內(nèi)存贝奇,2.工作內(nèi)存。每個(gè)線程的工作內(nèi)存都是獨(dú)立的靠胜,線程操作數(shù)據(jù)只能在工作內(nèi)存中計(jì)算掉瞳,然后刷入到主存。這是 Java 內(nèi)存模型定義的線程基本工作方式浪漠。
3. Java 內(nèi)存模型定義了哪些東西陕习?
實(shí)際上,整個(gè) Java 內(nèi)存模型圍繞了3個(gè)特征建立起來的郑藏。這三個(gè)特征是整個(gè)Java并發(fā)的基礎(chǔ)衡查。
原子性,可見性必盖,有序性拌牲。
原子性(Atomicity)
什么是原子性,其實(shí)這個(gè)原子性和事務(wù)處理中的原子性定義基本是一樣的歌粥。指的是一個(gè)操作是不可中斷的塌忽,不可分割的。即使在多個(gè)線程一起執(zhí)行的時(shí)候失驶,一個(gè)操作一旦開始土居,就不會(huì)被其他線程干擾。
我們大致可以認(rèn)為基本數(shù)據(jù)類型的訪問讀寫是具備原子性的(但是嬉探,如果你在32位虛擬機(jī)上計(jì)算 long 和 double 就不一樣了)擦耀,因?yàn)?java 虛擬機(jī)規(guī)范中,對(duì) long 和 double 的操作沒有強(qiáng)制定義要原子性的涩堤,但是強(qiáng)烈建議使用原子性的眷蜓。因此,大部分商用的虛擬機(jī)基本都實(shí)現(xiàn)了原子性胎围。
如果用戶需要操作一個(gè)更到的范圍保證原子性吁系,那么,Java 內(nèi)存模型提供了 lock 和 unlock (這是8種內(nèi)存操操作中的2種)操作來滿足這種需求白魂,但是沒有提供給程序員這兩個(gè)操作汽纤,提供了更抽象的 monitorenter 和 moniterexit 兩個(gè)字節(jié)碼指令,也就是 synchronized 關(guān)鍵字福荸。因此在 synchronized 塊之間的操作都是原子性的蕴坪。
可見性(Visibility)
可見性是指當(dāng)一個(gè)線程修改了共享變量的值,其他線程能夠立即得知這個(gè)修改敬锐,Java 內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存辞嗡,在變量讀取前從主內(nèi)存刷新變量值捆等,這種依賴主內(nèi)存作為傳遞媒介的方式來實(shí)習(xí)那可見性的。無論是普通變量還是 volatile 變量都是如此续室。他們的區(qū)別在于:volatile 的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每次是使用前都能從主內(nèi)存刷新谒养,因此挺狰,可以說 volatile 保證了多線程操作時(shí)變量的可見性,而普通變量則不能保證這一點(diǎn)买窟。
除了 volatile 之外丰泊, synchronized 和 final 也能實(shí)現(xiàn)可見性。同步塊的可見性是由 對(duì)一個(gè)變量執(zhí)行 unlock 操作之前始绍,必須先把此變量同步回主內(nèi)存種(執(zhí)行 store瞳购, write 操作)。
有序性(Ordering)
有序性這個(gè)問題我們?cè)谧钌厦嬲f硬件的時(shí)候說過亏推,CPU 會(huì)調(diào)整指令順序学赛,同樣的 Java 虛擬機(jī)同樣也會(huì)調(diào)整字節(jié)碼順序,但這種調(diào)整在單線程里時(shí)感知不到的吞杭,除非在多線程程序中盏浇,這種調(diào)整會(huì)帶來一些意想不到的錯(cuò)誤。
Java 提過了兩個(gè)關(guān)鍵字來保證多個(gè)線程之間操作的有序性芽狗,volatile 關(guān)鍵字本身就包含了禁止重排序的語義绢掰,而 synchronized 則是由 “一個(gè)變量同一時(shí)刻只允許一條線程對(duì)其進(jìn)行 lock 操作”這個(gè)規(guī)則獲得的。這條規(guī)則決定了同一個(gè)鎖的兩個(gè)同步塊只能串行的進(jìn)入童擎。
好了滴劲,介紹完了 JMM 的三種基本特征。不知道大家有沒有發(fā)現(xiàn)顾复,volatile 保證了可見性和有序性班挖,synchronized 則3個(gè)特性都保證了,堪稱萬能捕透。而且 synchronized 使用方便聪姿。但是,仍然要警惕他對(duì)性能的影響乙嘀。
4. Java內(nèi)存模型引出的 Happen-Before 原則是什么末购?
說到有序性,注意虎谢,我們說有序性可以通過 volatile 和 synchronized 來實(shí)現(xiàn)盟榴,但是我們不可能所有的代碼都靠這兩個(gè)關(guān)鍵字。實(shí)際上婴噩,Java 語言已對(duì)重排序或者說有序性做了規(guī)定擎场,這些規(guī)定在虛擬機(jī)優(yōu)化的時(shí)候是不能違背的羽德。
- 程序次序原則:一個(gè)線程內(nèi),按照程序代碼順序迅办,書寫在前面的操作先發(fā)生于書寫在后面的操作宅静。
- volatile 規(guī)則:volatile 變量的寫,先發(fā)生于讀站欺,這保證了 volatile 變量的可見性姨夹。
- 鎖規(guī)則:解鎖(unlock) 必然發(fā)生在隨后的加鎖(lock)前。
- 傳遞性:A先于B矾策,B先于C磷账,那么A必然先于C。
- 線程的 start 方法先于他的每一個(gè)動(dòng)作贾虽。
- 線程的所有操作先于線程的終結(jié)逃糟。
- 線程的中斷(interrupt())先于被中斷的代碼。
- 對(duì)象的構(gòu)造函數(shù)蓬豁,結(jié)束先于 finalize 方法绰咽。
5. Happen-Before 引出的 volatile 又是什么?
我們?cè)谇懊媲斐荆f了很多的 volatile 關(guān)鍵字剃诅,可見這個(gè)關(guān)鍵字非常的重要,但似乎他的使用頻率比 synchronized
少多了驶忌,我們知道了這個(gè)關(guān)鍵字可以做什么呢矛辕?
volatile 可以實(shí)現(xiàn)線程的可見性,還可以實(shí)現(xiàn)線程的有序性付魔。但是不能實(shí)現(xiàn)原子性聊品。
我們還是直接寫一段代碼吧!
package cn.think.in.java.two;
/**
* volatile 不能保證原子性几苍,只能遵守 hp 原則 保證單線程的有序性和可見性翻屈。
*/
public class MultitudeTest {
static volatile int i = 0;
static class PlusTask implements Runnable {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
// plusI();
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int j = 0; j < 10; j++) {
threads[j] = new Thread(new PlusTask());
threads[j].start();
}
for (int j = 0; j < 10; j++) {
threads[j].join();
}
System.out.println(i);
}
// static synchronized void plusI() {
// i++;
// }
}
我們啟動(dòng)了10個(gè)線程分別對(duì)一個(gè) int 變量進(jìn)行 ++ 操作,注意妻坝,++ 符號(hào)不是原子的伸眶。然后,主線程等待在這10個(gè)線程上刽宪,執(zhí)行結(jié)束后打印 int 值厘贼。你會(huì)發(fā)現(xiàn),無論怎么運(yùn)行都到不了10000圣拄,因?yàn)樗皇窃拥淖旖铡T趺蠢斫饽兀?/p>
i++ 等于 i = i + 1;
虛擬機(jī)首先讀取 i 的值,然后在 i 的基礎(chǔ)上加1岳掐,請(qǐng)注意凭疮,volatile 保證了線程讀取的值是最新的,當(dāng)線程讀取 i 的時(shí)候串述,該值確實(shí)是最新的执解,但是有10個(gè)線程都去讀了,他們讀到的都是最新的纲酗,并且同時(shí)加1材鹦,這些操作不違法 volatile 的定義。最終出現(xiàn)錯(cuò)誤耕姊,可以說是我們使用不當(dāng)。
樓主也在測(cè)試代碼中加入了一個(gè)同步方法栅葡,同步方法能夠保證原子性茉兰。當(dāng)for循環(huán)中執(zhí)行的不是i++,而是 plusI 方法欣簇,那么結(jié)果就會(huì)準(zhǔn)確了规脸。
那么,什么時(shí)候用 volatile 呢熊咽?
運(yùn)算結(jié)果并不依賴變量的當(dāng)前值莫鸭,或者能夠確保只有單一的線程修改變量的值。
我們程序的情況就是横殴,運(yùn)算結(jié)果依賴 i 當(dāng)前的值被因,如果改為 原子操作: i = j,那么結(jié)果就會(huì)是正確的 9999.
比如下面這個(gè)程序就是使用 volatile 的范例:
package cn.think.in.java.two;
/**
* java 內(nèi)存模型:
* 單線程下會(huì)重排序衫仑。
* 下面這段程序再 -server 模式下會(huì)優(yōu)化代碼(重排序)梨与,導(dǎo)致永遠(yuǎn)死循環(huán)。
*/
public class JMMDemo {
// static boolean ready;
static volatile boolean ready;
static int num;
static class ReaderThread extends Thread {
public void run() {
while (!ready) {
}
System.out.println(num);
}
}
public static void main(String[] args) throws InterruptedException {
new ReaderThread().start();
Thread.sleep(1000);
num = 32;
ready = true;
Thread.sleep(1000);
Thread.yield();
}
}
這段程序很有意思文狱,我們使用 volatile 變量來控制流程粥鞋,最終的正確結(jié)果是32,但是請(qǐng)注意瞄崇,如果你沒有使用 volatile 關(guān)鍵字呻粹,并且虛擬機(jī)啟動(dòng)的時(shí)候加入了 -server參數(shù),這段程序?qū)⒂肋h(yuǎn)不會(huì)結(jié)束苏研,因?yàn)樗麜?huì)被 JIT 優(yōu)化并且另一個(gè)線程永遠(yuǎn)無法看到變量的修改(JIT 會(huì)忽略他認(rèn)為無效的代碼)等浊。當(dāng)然,當(dāng)你修改為 volatile 就沒有任何問題了楣富。
通過上面的代碼凿掂,我們知道了,volatile 確實(shí)不能保證原子性,但是能保證有序性和可見性庄萎。那么是怎么實(shí)現(xiàn)的呢踪少?
怎么保證有序性呢?實(shí)際上糠涛,在操作 volatile 關(guān)鍵字變量前后的匯編代碼中援奢,會(huì)有一個(gè) lock 前綴,根據(jù) intel IA32 手冊(cè)忍捡,lock 的作用是 使得 本 CPU 的Cache 寫入了內(nèi)存集漾,該寫入動(dòng)作也會(huì)引起別的CPU或者別的內(nèi)核無效化其Cache,別的CPU需要重新獲取Cache砸脊。這樣就實(shí)現(xiàn)了可見性具篇。可見底層還是使用的 CPU 的指令凌埂。
如何實(shí)現(xiàn)有序性呢驱显?同樣是lock 指令,這個(gè)指令還相當(dāng)于一個(gè)內(nèi)存屏障(大多數(shù)現(xiàn)代計(jì)算機(jī)為了提高性能而采取亂序執(zhí)行瞳抓,這使得內(nèi)存屏障成為必須埃疫。語義上,內(nèi)存屏障之前的所有寫操作都要寫入內(nèi)存孩哑;內(nèi)存屏障之后的讀操作都可以獲得同步屏障之前的寫操作的結(jié)果栓霜。因此,對(duì)于敏感的程序塊横蜒,寫操作之后胳蛮、讀操作之前可以插入內(nèi)存屏障
),指的是愁铺,重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置鹰霍。只有一個(gè)CPU訪問內(nèi)存時(shí),并不需要內(nèi)存屏障茵乱;但如果有兩個(gè)或者更多CPU訪問同一塊內(nèi)存茂洒,且其中有一個(gè)在觀測(cè)另一個(gè),就需要內(nèi)存屏障來保證了瓶竭。
因此請(qǐng)不要隨意使用 volatile 變量督勺,這會(huì)導(dǎo)致 JIT 無法優(yōu)化代碼,并且會(huì)插入很多的內(nèi)存屏障指令斤贰,降低性能智哀。
6. 總結(jié)
首先 JMM 是抽象化了硬件的內(nèi)存模型(使用了多級(jí)緩存導(dǎo)致出現(xiàn)緩存一致性協(xié)議),屏蔽了各個(gè) CPU 和操作系統(tǒng)的差異荧恍。
Java 內(nèi)存模型指的是:在特定的協(xié)議下對(duì)內(nèi)存的訪問過程瓷叫。也就是線程的工作內(nèi)存和主存直接的操作順序屯吊。
JMM 主要圍繞著原子性,可見性摹菠,有序性來設(shè)置規(guī)范盒卸。
synchronized 可以實(shí)現(xiàn)這3個(gè)功能,而 volatile 只能實(shí)現(xiàn)可見性和有序性次氨。final 也能是實(shí)現(xiàn)可見性蔽介。
Happen-Before 原則規(guī)定了哪些是虛擬機(jī)不能重排序的,其中包括了鎖的規(guī)定煮寡,volatile 變量的讀與寫規(guī)定虹蓄。
而 volatile 我們也說了,不能保證原子性幸撕,所以使用的時(shí)候需要注意薇组。volatile 底層的實(shí)現(xiàn)還是 CPU 的 lock 指令,通過刷新其余的CPU 的Cache 保證可見性坐儿,通過內(nèi)存柵欄保證了有序性体箕。
總的來說,這3個(gè)概念可以說息息相關(guān)挑童。他們之間互相依賴。所以樓主放在了一篇來寫跃须,但這可能會(huì)導(dǎo)致有所疏漏站叼,但不妨礙我們了解整個(gè)的概念」矫瘢可以說尽楔,JMM 是所有并發(fā)編程的基礎(chǔ),如果不了解 JMM第练,根本不可能高效并發(fā)阔馋。
當(dāng)然,我們這篇文章還是不夠底層娇掏,并沒有剖析 JVM 內(nèi)部是怎么實(shí)現(xiàn)的呕寝,今天已經(jīng)很晚了,有機(jī)會(huì)婴梧,我們一起進(jìn)入 JVM 源碼查看他們的底層實(shí)現(xiàn)下梢。
good luck!H洹D踅!