JAVA內(nèi)存模型(JMM)詳解
前言
開(kāi)篇一個(gè)例子,我看看都有誰(shuí)會(huì)?如果不會(huì)的,或者不知道原理的,還是老老實(shí)實(shí)看完這篇文章吧。
@Slf4j(topic = "c.VolatileTest") public class VolatileTest { static boolean run = true; public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { while (run) { // do other things } // ?????? 這行會(huì)打印嗎? log.info("done ....."); }); t.start(); Thread.sleep(1000); // 設(shè)置run = false run = false; } }
main
函數(shù)中新開(kāi)個(gè)線(xiàn)程根據(jù)標(biāo)位run
循環(huán),主線(xiàn)程中sleep
一秒,然后設(shè)置run=false
,大家認(rèn)為會(huì)打印"done .......
"嗎?
答案就是不會(huì)打印,為什么呢?
JAVA并發(fā)三大特性
我們先來(lái)解釋下上面問(wèn)題的原因,如下圖所示,
現(xiàn)代的CPU架構(gòu)基本有多級(jí)緩存機(jī)制,t線(xiàn)程會(huì)將run
加載到高速緩存中,然后主線(xiàn)程修改了主內(nèi)存的值為false,導(dǎo)致緩存不一致,但是t線(xiàn)程依然是從工作內(nèi)存中的高速緩存讀取run
的值,最終無(wú)法跳出循環(huán)。
可見(jiàn)性
正如上面的例子,由于不做任何處理,一個(gè)線(xiàn)程能否立刻看到另外一個(gè)線(xiàn)程修改的共享變量值,我們稱(chēng)為"可見(jiàn)性"。
如果在并發(fā)程序中,不做任何處理,那么就會(huì)帶來(lái)可見(jiàn)性問(wèn)題,具體如何處理,見(jiàn)后文。
有序性
有序性是指程序按照代碼的先后順序執(zhí)行。但是編譯器或者處理器出于性能原因,改變程序語(yǔ)句的先后順序,比如代碼順序"a=1; b=2;
",但是指令重排序后,有可能會(huì)變成"b=2;a=1
", 那么這樣在并發(fā)情況下,會(huì)有問(wèn)題嗎?
在單線(xiàn)程情況下,指令重排序不會(huì)有任何影響。但是在并發(fā)情況下,可能會(huì)導(dǎo)致一些意想不到的bug。比如下面的例子:
public class Singleton { static Singleton instance; static Singleton getInstance(){ if (instance == null) { synchronized(Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; } }
假設(shè)有兩個(gè)線(xiàn)程 A、B 同時(shí)調(diào)用 getInstance()
方法,正常情況下,他們都可以拿到instance
實(shí)例。
但往往bug就在一些極端的異常情況,比如new Singleton()
這個(gè)操作,實(shí)際會(huì)有下面3個(gè)步驟:
分配一塊內(nèi)存 M;
在內(nèi)存 M 上初始化 Singleton
對(duì)象;
然后 M 的地址賦值給 instance
變量。
現(xiàn)在發(fā)生指令重排序,順序變?yōu)橄旅娴姆绞剑?/p>
分配一塊內(nèi)存 M;
將 M 的地址賦值給 instance 變量;
最后在內(nèi)存 M 上初始化 Singleton 對(duì)象。
優(yōu)化后會(huì)導(dǎo)致什么問(wèn)題呢?我們假設(shè)線(xiàn)程 A 先執(zhí)行 getInstance()
方法,當(dāng)執(zhí)行完指令 2 時(shí)恰好發(fā)生了線(xiàn)程切換,切換到了線(xiàn)程 B 上;如果此時(shí)線(xiàn)程 B 也執(zhí)行 getInstance()
方法,那么線(xiàn)程 B 在執(zhí)行第一個(gè)判斷時(shí)會(huì)發(fā)現(xiàn) instance != null ,所以直接返回 instance,而此時(shí)的 instance
是沒(méi)有初始化過(guò)的,如果我們這個(gè)時(shí)候訪(fǎng)問(wèn) instance 的成員變量就可能觸發(fā)空指針異常。
這就是并發(fā)情況下,有序性帶來(lái)的一個(gè)問(wèn)題,這種情況又該如何處理呢?
當(dāng)然,指令重排序并不會(huì)瞎排序,處理器在進(jìn)行重排序時(shí),必須要考慮指令之間的數(shù)據(jù)依賴(lài)性。
原子性
如上圖所示,在多線(xiàn)程的情況下,CPU資源會(huì)在不同的線(xiàn)程間切換。那么這樣也會(huì)導(dǎo)致意向不到的問(wèn)題。
比如你認(rèn)為的一行代碼:count += 1
,實(shí)際上涉及了多條CPU指令:
指令 1:首先,需要把變量 count 從內(nèi)存加載到 CPU 的寄存器; 指令 2:之后,在寄存器中執(zhí)行 +1 操作; 指令 3:最后,將結(jié)果寫(xiě)入內(nèi)存(緩存機(jī)制導(dǎo)致可能寫(xiě)入的是 CPU 緩存而不是內(nèi)存)。
操作系統(tǒng)做任務(wù)切換,可以發(fā)生在任何一條CPU 指令執(zhí)行完。假設(shè) count=0
,如果線(xiàn)程 A 在指令 1 執(zhí)行完后做線(xiàn)程切換,線(xiàn)程 A 和線(xiàn)程 B 按照下圖的序列執(zhí)行,那么我們會(huì)發(fā)現(xiàn)兩個(gè)線(xiàn)程都執(zhí)行了 count+=1
的操作,但是得到的結(jié)果不是我們期望的 2,而是 1。
我們潛意識(shí)認(rèn)為的這個(gè)
count+=1
操作是一個(gè)不可分割的整體,就像一個(gè)原子一樣,我們把一個(gè)或者多個(gè)操作在 CPU 執(zhí)行的過(guò)程中不被中斷的特性稱(chēng)為原子性。但實(shí)際情況就是不做任何處理的話(huà),在并發(fā)情況下CPU進(jìn)行切換,導(dǎo)致出現(xiàn)原子性的問(wèn)題,我們一般通過(guò)加鎖解決,這個(gè)不是本文的重點(diǎn)。
Java內(nèi)存模型真面目
前面講解并發(fā)的三大特性,其中原子性問(wèn)題可以通過(guò)加鎖的方式解決,那么可見(jiàn)性和有序性有什么解決的方案呢?其實(shí)也很容易想到,可見(jiàn)性是因?yàn)榫彺鎸?dǎo)致,有序性是因?yàn)榫幾g優(yōu)化指令重排序?qū)е?,那么是不是可以讓程序員按需禁用緩存以及編譯優(yōu)化, 因?yàn)橹挥谐绦騿T知道什么情況下會(huì)出現(xiàn)問(wèn)題 。 順著這個(gè)思路,就提出了JAVA內(nèi)存模型(JMM)規(guī)范。
Java 內(nèi)存模型是 Java Memory Model(JMM)
,本身是一種抽象的概念,實(shí)際上并不存在,描述的是一組規(guī)則或規(guī)范,通過(guò)這組規(guī)范定義了程序中各個(gè)變量(包括實(shí)例字段,靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素)的訪(fǎng)問(wèn)方式。
默認(rèn)情況下,JMM中的內(nèi)存機(jī)制如下:
系統(tǒng)存在一個(gè)主內(nèi)存(Main Memory
),Java 中所有變量都存儲(chǔ)在主存中,對(duì)于所有線(xiàn)程都是共享的 每條線(xiàn)程都有自己的工作內(nèi)存(Working Memory
),工作內(nèi)存中保存的是主存中某些變量的拷貝 線(xiàn)程對(duì)所有變量的操作都是先對(duì)變量進(jìn)行拷貝,然后在工作內(nèi)存中進(jìn)行,不能直接操作主內(nèi)存中的變量 線(xiàn)程之間無(wú)法相互直接訪(fǎng)問(wèn),線(xiàn)程間的通信(傳遞)必須通過(guò)主內(nèi)存來(lái)完成
同時(shí),JMM規(guī)范了 JVM 如何提供按需禁用緩存和編譯優(yōu)化的方法,主要是通過(guò)volatile
、synchronized
和 final
三個(gè)關(guān)鍵字,那具體的規(guī)則是什么樣的呢?
JMM 中的主內(nèi)存、工作內(nèi)存與 JVM 中的 Java 堆、棧、方法區(qū)等并不是同一個(gè)層次的內(nèi)存劃分,這兩者基本上是沒(méi)有關(guān)系的。
Happens-Before規(guī)則
JMM本質(zhì)上包含了一些規(guī)則,那這個(gè)規(guī)則就是大家有所耳聞的Happens-Before
規(guī)則,大家都理解了些規(guī)則嗎?
Happens-Before
規(guī)則,可以簡(jiǎn)單理解為如果想要A線(xiàn)程發(fā)生在B線(xiàn)程前面,也就是B線(xiàn)程能夠看到A線(xiàn)程,需要遵循6個(gè)原則。如果不符合 happens-before 規(guī)則,JMM 并不能保證一個(gè)線(xiàn)程的可見(jiàn)性和有序性。
1.程序的順序性規(guī)則
在一個(gè)線(xiàn)程中,邏輯上書(shū)寫(xiě)在前面的操作先行發(fā)生于書(shū)寫(xiě)在后面的操作。
這個(gè)規(guī)則很好理解,同一個(gè)線(xiàn)程中他們是用的同一個(gè)工作緩存,是可見(jiàn)的,并且多個(gè)操作之間有先后依賴(lài)關(guān)系,則不允許對(duì)這些操作進(jìn)行重排序。
2. volatile
變量規(guī)則
指對(duì)一個(gè) volatile
變量的寫(xiě)操作, Happens-Before
于后續(xù)對(duì)這個(gè) volatile
變量的讀操作。
怎么理解呢?比如線(xiàn)程A對(duì)volatile
變量進(jìn)行寫(xiě)操作,那么線(xiàn)程B讀取這個(gè)volatile
變量是可見(jiàn)的,就是說(shuō)能夠讀取到最新的值。
3.傳遞性
這條規(guī)則是指如果 A Happens-Before B
,且 B Happens-Before C
,那么 A Happens-Before C
。
這個(gè)規(guī)則也比較容易理解,不展開(kāi)討論了。
鎖的規(guī)則
這條規(guī)則是指對(duì)一個(gè)鎖的解鎖 Happens-Before
于后續(xù)對(duì)這個(gè)鎖的加鎖,這里的鎖要是同一把鎖, 而且用synchronized
或者ReentrantLock
都可以。
如下代碼的例子:
synchronized (this) { // 此處自動(dòng)加鎖 // x 是共享變量, 初始值 =10 if (this.x < 12) { this.x = 12; } } // 此處自動(dòng)解鎖
假設(shè) x 的初始值是 8,線(xiàn)程 A 執(zhí)行完代碼塊后 x 的值會(huì)變成 12(執(zhí)行完自動(dòng)釋放鎖) 線(xiàn)程 B 進(jìn)入代碼塊時(shí),能夠看到線(xiàn)程 A 對(duì) x 的寫(xiě)操作,也就是線(xiàn)程 B 能夠看到 x==12
。
5.線(xiàn)程 start()
規(guī)則
主線(xiàn)程 A 啟動(dòng)子線(xiàn)程 B 后,子線(xiàn)程 B 能夠看到主線(xiàn)程在啟動(dòng)子線(xiàn)程 B 前的操作。
這個(gè)規(guī)則也很容易理解,線(xiàn)程 A 調(diào)用線(xiàn)程 B 的 start() 方法(即在線(xiàn)程 A 中啟動(dòng)線(xiàn)程 B),那么該 start() 操作 Happens-Before
于線(xiàn)程 B 中的任意操作。
6.線(xiàn)程 join()
規(guī)則
線(xiàn)程 A 中,調(diào)用線(xiàn)程 B 的 join()
并成功返回,那么線(xiàn)程 B 中的任意操作 Happens-Before
于該 join() 操作的返回。
使用JMM規(guī)則
我們現(xiàn)在已經(jīng)基本講清楚了JAVA內(nèi)存模型規(guī)范,以及里面關(guān)鍵的Happens-Before
規(guī)則,那有啥用呢?回到前言的問(wèn)題中,我們是不是可以使用目前學(xué)到的關(guān)于JMM的知識(shí)去解決這個(gè)問(wèn)題。
方案一: 使用volatile
根據(jù)JMM的第2條規(guī)則,主線(xiàn)程寫(xiě)了volatile
修飾的run
變量,后面的t線(xiàn)程讀取的時(shí)候就可以看到了。
方案二:使用鎖
利用synchronized
鎖的規(guī)則,主線(xiàn)程釋放鎖,那么后續(xù)t線(xiàn)程加鎖就可以看到之前的內(nèi)容了。
小結(jié):
volatile
關(guān)鍵字
保證可見(jiàn)性 不保證原子性 保證有序性(禁止指令重排)
volatile
修飾的變量進(jìn)行讀操作與普通變量幾乎沒(méi)什么差別,但是寫(xiě)操作相對(duì)慢一些,因?yàn)樾枰诒镜卮a中插入很多內(nèi)存屏障來(lái)保證指令不會(huì)發(fā)生亂序執(zhí)行,但是開(kāi)銷(xiāo)比鎖要小。volatile
的性能遠(yuǎn)比加鎖要好。
synchronized
關(guān)鍵字
保證可見(jiàn)性 不保證原子性 保證有序性
加了鎖之后,只能有一個(gè)線(xiàn)程獲得到了鎖,獲得不到鎖的線(xiàn)程就要阻塞,所以同一時(shí)間只有一個(gè)線(xiàn)程執(zhí)行,相當(dāng)于單線(xiàn)程,由于數(shù)據(jù)依賴(lài)性的存在,單線(xiàn)程的指令重排是沒(méi)有問(wèn)題的。
線(xiàn)程加鎖前,將清空工作內(nèi)存中共享變量的值,使用共享變量時(shí)需要從主內(nèi)存中重新讀取最新的值;線(xiàn)程解鎖前,必須把共享變量的最新值刷新到主內(nèi)存中。
總結(jié)
本文講解了JAVA并發(fā)的3大特性,可見(jiàn)性、有序性和原子性。從而引出了JAVA內(nèi)存模型規(guī)范,這主要是為了解決并發(fā)情況下帶來(lái)的可見(jiàn)性和有序性問(wèn)題,主要就是定義了一些規(guī)則,需要我們程序員懂得這些規(guī)則,然后根據(jù)實(shí)際場(chǎng)景去使用,就是使用volatile
、synchronized
、final
關(guān)鍵字,主要final關(guān)鍵字也會(huì)讓其他線(xiàn)程可見(jiàn),并且保證有序性。那么具體他們底層的實(shí)現(xiàn)是什么,是如何保證可見(jiàn)和有序的,我們后面詳細(xì)講解。
到此這篇關(guān)于JAVA內(nèi)存模型(JMM)詳解的文章就介紹到這了,更多相關(guān)JAVA內(nèi)存模型 JMM內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 淺析Java內(nèi)存模型與垃圾回收
- Java 高并發(fā)三:Java內(nèi)存模型和線(xiàn)程安全詳解
- 在Java內(nèi)存模型中測(cè)試并發(fā)程序代碼
- Java8內(nèi)存模型PermGen Metaspace實(shí)例解析
- Java內(nèi)存模型JMM詳解
- Java內(nèi)存模型與JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)的區(qū)別詳解
- Java內(nèi)存區(qū)域和內(nèi)存模型講解
- Java內(nèi)存模型之happens-before概念詳解
- Java內(nèi)存模型(JMM)及happens-before原理
- 細(xì)談java同步之JMM(Java Memory Model)
- 學(xué)習(xí)Java內(nèi)存模型JMM心得
相關(guān)文章
Spring?Framework六種常見(jiàn)設(shè)計(jì)模式
設(shè)計(jì)模式是軟件開(kāi)發(fā)的重要組成部分,本文借助spring來(lái)講解這個(gè)框架的設(shè)計(jì)模式,通過(guò)本文我們探討了spring如何利用這些模式來(lái)提供這些豐富的功能,對(duì)本文感興趣的朋友跟隨小編一起看看吧2023-06-06java抓取鼠標(biāo)事件和鼠標(biāo)滾輪事件示例
這篇文章主要介紹了java抓取鼠標(biāo)事件和鼠標(biāo)滾輪事件示例,需要的朋友可以參考下2014-05-05springboot 自定義配置Boolean屬性不生效的解決
這篇文章主要介紹了springboot 自定義配置Boolean屬性不生效的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03java面試散列表及樹(shù)所對(duì)應(yīng)容器類(lèi)及HashMap沖突解決全面分析
這篇文章主要介紹了java面試中的java散列表及樹(shù)所對(duì)應(yīng)容器類(lèi)與HashMap沖突解決的問(wèn)題總結(jié),有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-10-10Spring Boot Feign服務(wù)調(diào)用之間帶token問(wèn)題
這篇文章主要介紹了Spring Boot Feign服務(wù)調(diào)用之間帶token的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09Spring?Cloud?Eureka高可用配置(踩坑記錄)
在進(jìn)行Eureka高可用配置時(shí),控制臺(tái)一直出現(xiàn)“......”的錯(cuò)誤,但是在瀏覽器中輸入地址:peer1:8761 卻是可正常運(yùn)行,這篇文章主要介紹了Spring?Cloud踩坑之Eureka高可用配置,需要的朋友可以參考下2023-08-08