Java中的內(nèi)存模型JMM詳細(xì)解讀
一、CPU緩存一致性問題
1. CPU緩存模型
CPU Cache 通常分為三級(jí)緩存:L1 Cache、L2 Cache、L3 Cache,級(jí)別越低的離 CPU 核心越近,訪問速度也快,但是存儲(chǔ)容量相對(duì)就會(huì)越小。其中,在多核心的 CPU 里,每個(gè)核心都有各自的 L1/L2 Cache,而 L3 Cache 是所有核心共享使用的。
2. MESI緩存一致性協(xié)議
多核CPU緩存則必然會(huì)有緩存與主存之間的一致性的問題,例如在核心1的L1/L2 cache中修改了某項(xiàng)數(shù)據(jù)但還沒寫回主存,那么核心2再讀取這項(xiàng)數(shù)據(jù)時(shí)則讀的舊的錯(cuò)誤數(shù)據(jù)。
要想實(shí)現(xiàn)緩存一致性,要滿足以下兩點(diǎn):
寫傳播:某個(gè) CPU 核心里的 Cache 數(shù)據(jù)更新時(shí),必須要傳播到其他核心的 Cache。
事務(wù)的串行化:多個(gè) CPU 核心對(duì)一個(gè)數(shù)據(jù)的操作順序,必須在其他核心看起來順序是一樣的。
基于總線嗅探機(jī)制的緩存一致性協(xié)議 MESI 就滿足上面了這兩點(diǎn)。CPU緩存中的每塊數(shù)據(jù)中都有如下其中的一個(gè)狀態(tài)標(biāo)記,當(dāng)數(shù)據(jù)變化時(shí)通過總線嗅探監(jiān)聽機(jī)制使其它CPU核心感知到并修改緩存數(shù)據(jù)的狀態(tài):
3. 弱緩存一致性
上述MESI協(xié)議雖然可以保證緩存的一致性,但又會(huì)影響性能,因此現(xiàn)代計(jì)算機(jī)中并不是完全遵守。關(guān)于這個(gè)問題的發(fā)展歷程如下:
CPU 從單核發(fā)展為多核,導(dǎo)致出現(xiàn)了多個(gè)核間的緩存一致性問題 --> 為了解決緩存一致性問題,提出了 MESI 協(xié)議 --> 完全遵守 MESI 又會(huì)給 CPU 帶來性能問題 --> CPU 設(shè)計(jì)者為了提高性能又在cache基礎(chǔ)上增加 store buffer 和 invalid queue --> 又導(dǎo)致了緩存的順序一致性變?yōu)榱巳蹙彺嬉恢滦?--> 需要緩存的順序一致性的,就需要軟件工程師自己在合適的地方添加內(nèi)存屏障,volatile 的作用之一就是給虛擬機(jī)看讓其在對(duì)應(yīng)的指令加入內(nèi)存屏障。防止cpu級(jí)別的重排序,從而避免緩存一致性問題。
因此由于CPU弱緩存一致性的問題,在多線程中,一個(gè)線程對(duì)于一個(gè)共享變量的修改對(duì)其它線程可能是不可見的。
二、指令的重排序問題
指令重排序: 在不影響單線程程序執(zhí)行結(jié)果的前提下,計(jì)算機(jī)為了最大限度的發(fā)揮機(jī)器性能,會(huì)對(duì)機(jī)器指令重排序優(yōu)化 Java 源代碼會(huì)經(jīng)歷 編譯器優(yōu)化重排 —> 指令并行重排 —> 內(nèi)存系統(tǒng)重排 的過程,最終才變成操作系統(tǒng)可執(zhí)行的指令序列。
指令重排序會(huì)保證串行語義一致,但是沒有義務(wù)保證多線程間的語義也一致 ,所以在多線程下,指令重排序可能會(huì)導(dǎo)致一些問題。
例如如下創(chuàng)建單例對(duì)象的代碼
uniqueInstance = new Singleton();
這段代碼其實(shí)是分為三步執(zhí)行:
- 為 uniqueInstance 分配內(nèi)存空間
- 初始化uniqueInstance
- 將 uniqueInstance指向分配的內(nèi)存地址
但是由于 JVM 具有指令重排的特性,執(zhí)行順序有可能變成 1->3->2。指令重排在單線程環(huán)境下不會(huì)出現(xiàn)問題,但是在多線程環(huán)境下會(huì)導(dǎo)致一個(gè)線程獲得還沒有初始化的實(shí)例。例如,線程 T1 執(zhí)行了 1 和 3,此時(shí) T2 調(diào)用 getUniqueInstance() 后發(fā)現(xiàn) uniqueInstance 不為空,因此返回 uniqueInstance,但此時(shí) uniqueInstance 還未被初始化,從而導(dǎo)致出錯(cuò)。
三、Java 內(nèi)存模型(JMM)詳解
Java是跨平臺(tái)的,為解決不同平臺(tái)下上述CPU弱緩存一致性帶來的共享變量可見性以及指令的重排序等問題,并且方便程序員更加安全高效地實(shí)現(xiàn)多線程編程,Java提供一套內(nèi)存模型以及并發(fā)編程規(guī)范以屏蔽系統(tǒng)差異。
對(duì)于 Java 開發(fā)者說,你不需要了解底層原理,直接使用并發(fā)相關(guān)的一些關(guān)鍵字和類(比如 volatile、synchronized、各種 Lock)即可開發(fā)出并發(fā)安全的程序。
1. Java 內(nèi)存模型
Java 對(duì)內(nèi)存的抽象模型如下,每個(gè)線程都有一塊自己的私有內(nèi)存(也稱為工作內(nèi)存),當(dāng)線程使用變量時(shí),會(huì)把主內(nèi)存里面的變量復(fù)制到工作內(nèi)存,線程讀寫變量時(shí)操作的是自己工作內(nèi)存中的變量。線程的工作內(nèi)存實(shí)際上就是對(duì)CPU緩存和寄存器的統(tǒng)一抽象。
為實(shí)現(xiàn)線程工作內(nèi)存與主內(nèi)存的同步,Java規(guī)范在內(nèi)存模型中定義了以下八種同步操作(了解即可,無需死記硬背):
- lock(鎖定): 作用于主內(nèi)存中的變量,將他標(biāo)記為一個(gè)線程獨(dú)享變量。
- unlock(解鎖): 作用于主內(nèi)存中的變量,解除變量的鎖定狀態(tài),被解除鎖定狀態(tài)的變量才能被其他線程鎖定。
- read(讀?。鹤饔糜谥鲀?nèi)存的變量,它把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的 load 動(dòng)作使用。
- load(載入):把 read 操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量的副本中。
- use(使用):把工作內(nèi)存中的一個(gè)變量的值傳給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)使用到變量的指令時(shí)都會(huì)使用該指令。
- assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作。
- store(存儲(chǔ)):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的 write 操作使用。
- write(寫入):作用于主內(nèi)存的變量,它把 store 操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。
我們?cè)诰帉懗绦虼a時(shí)使用volatile、synchronized和各種 Lock等關(guān)鍵字即可間接實(shí)現(xiàn)這些同步操作來解決前面提到的在多線程中可能會(huì)出現(xiàn)的問題。
以如下程序?yàn)槔?/p>
public class JMMTest { private boolean initFlag = false; // private volatile boolean initFlag = false;//volatile關(guān)鍵字可保證變量的可見性以及指令的有序性 public static void main(String[] args) throws InterruptedException { JMMTest jmmTest = new JMMTest(); new Thread(() -> { System.out.println("Thread1-start"); //線程2對(duì)flag的修改對(duì)線程1不可見,故會(huì)陷入死循環(huán) while (!jmmTest.initFlag){ } System.out.println("Thread1-end"); }).start(); Thread.sleep(100); new Thread(() -> { System.out.println("Thread2-start"); jmmTest.initFlag = true; System.out.println("Thread2-end"); }).start(); } }
當(dāng)成員變量initFlag沒有用volatile修飾時(shí),線程1首先用read操作從主內(nèi)存中讀取initFlag的值,然后用load操作加載到工作內(nèi)存的副本中,再用use操作使用值后進(jìn)入循環(huán),輪到線程2執(zhí)行,也是先read -> load -> use,然后用assign操作將從執(zhí)行引擎接收到的true值復(fù)制給工作內(nèi)存中的initFlag副本,最后在某個(gè)時(shí)候用store -> write操作寫入主內(nèi)存。但是此時(shí)線程1仍然讀取的是其工作內(nèi)存中的值,因此就陷入了死循環(huán)。
當(dāng)成員變量initFlag使用volatile修飾后,線程2修改initFlag后會(huì)立即寫回主內(nèi)存并且讓線程1中的變量副本失效,因此線程1需要從主內(nèi)存中重新讀取最新的值,以此實(shí)現(xiàn)了變量的可見性,從而能夠退出循環(huán)。
2. 內(nèi)存屏障
內(nèi)存屏障表示隔開兩個(gè)內(nèi)存同步操作,使其能夠有序執(zhí)行而不被重排序
內(nèi)存屏障只是一種規(guī)范,真正落地的實(shí)現(xiàn)屬于底層的細(xì)節(jié),比如volatile的內(nèi)存屏障底層是通過lock匯編指令實(shí)現(xiàn)的。
3. happens-before 原則
happens-before 原則表示程序中某些指令操作必發(fā)生在另一些指令操作前面,不允許重排序。
happens-before 原則的設(shè)計(jì)思想:
為了對(duì)編譯器和處理器的約束盡可能少,只要不改變程序的執(zhí)行結(jié)果(單線程程序和正確執(zhí)行的多線程程序),編譯器和處理器怎么進(jìn)行重排序優(yōu)化都行。
- 對(duì)于會(huì)改變程序執(zhí)行結(jié)果的重排序,JMM 要求編譯器和處理器必須禁止這種重排序。
- happens-before 的規(guī)則共 8 條,重點(diǎn)了解下面5 條即可:
- 程序順序規(guī)則 :一個(gè)線程內(nèi),按照代碼順序,書寫在前面的操作 happens-before 于書寫在后面的操作;
- 解鎖規(guī)則 :解鎖 happens-before 于加鎖;
- volatile 變量規(guī)則 :對(duì)一個(gè) volatile 變量的寫操作 happens-before 于后面對(duì)這個(gè) volatile 變量的讀操作。說白了就是對(duì) volatile 變量的寫操作的結(jié)果對(duì)于發(fā)生于其后的任何操作都是可見的。
- 傳遞規(guī)則 :如果 A happens-before B,且 B happens-before C,那么 A happens-before C;
- 線程啟動(dòng)規(guī)則 :Thread 對(duì)象的 start()方法 happens-before 于此線程的每一個(gè)動(dòng)作。
如果兩個(gè)操作不滿足任意一個(gè) happens-before 規(guī)則,那么這兩個(gè)操作就沒有順序的保障,JVM 可以對(duì)這兩個(gè)操作進(jìn)行重排序。程序員則基于happens-before規(guī)則提供的內(nèi)存可見性保證來編程。
四、并發(fā)編程的三個(gè)重要特性
1. 原子性
原子性:一次操作或者多次操作,要么所有的操作全部都得到執(zhí)行并且不會(huì)受到任何因素的干擾而中斷,要么都不執(zhí)行。 在 Java 中,可以借助synchronized 、各種 Lock 以及各種原子類實(shí)現(xiàn)原子性。 synchronized 和各種 Lock 可以保證任一時(shí)刻只有一個(gè)線程訪問該代碼塊,因此可以保障原子性。各種原子類是利用 CAS (compare and swap) 操作(可能也會(huì)用到 volatile或者final關(guān)鍵字)來保證原子操作。
2. 可見性
可見性:當(dāng)一個(gè)線程對(duì)共享變量進(jìn)行了修改,那么另外的線程都是立即可以看到修改后的最新值。 在 Java 中,可以借助synchronized 、volatile 以及各種 Lock 實(shí)現(xiàn)可見性。如果我們將變量聲明為 volatile ,這就指示 JVM,這個(gè)變量是共享且不穩(wěn)定的,每次使用它都到主存中進(jìn)行讀取。
3. 有序性
有序性:指令執(zhí)行順序在并發(fā)環(huán)境下依然能按預(yù)期執(zhí)行,不會(huì)因?yàn)橹嘏判蚨a(chǎn)生錯(cuò)亂。 由于指令重排序問題,代碼的執(zhí)行順序未必就是編寫代碼時(shí)候的順序。我們上面講重排序的時(shí)候也提到過:指令重排序可以保證串行語義一致,但是沒有義務(wù)保證多線程間的語義也一致 ,所以在多線程下,指令重排序可能會(huì)導(dǎo)致一些問題。在 Java 中,volatile 關(guān)鍵字可以禁止指令進(jìn)行重排序優(yōu)化。
到此這篇關(guān)于Java中的內(nèi)存模型JMM詳細(xì)解讀的文章就介紹到這了,更多相關(guān)Java內(nèi)存模型JMM內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java基于swing實(shí)現(xiàn)的彈球游戲代碼
這篇文章主要介紹了Java基于swing實(shí)現(xiàn)的彈球游戲代碼,包含了窗體界面設(shè)計(jì)與游戲的邏輯功能處理,具有不錯(cuò)的參考借鑒價(jià)值,需要的朋友可以參考下2014-11-11Spring實(shí)戰(zhàn)之XML與JavaConfig的混合配置詳解
大家都知道Spring的顯示配置方式有兩種,一種是基于XML配置,一種是基于JavaConfig的方式配置。那么下這篇文章主要給大家分別介紹如何在JavaConfig中引用XML配置的bean以及如何在XML配置中引用JavaConfig,需要的朋友可以參考下。2017-07-07SpringBoot測(cè)試時(shí)卡在Resolving Maven dependencies的問題
這篇文章主要介紹了SpringBoot測(cè)試時(shí)卡在Resolving Maven dependencies的問題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02Spring Boot2中如何優(yōu)雅地個(gè)性化定制Jackson實(shí)現(xiàn)示例
這篇文章主要為大家介紹了Spring Boot2中如何優(yōu)雅地個(gè)性化定制Jackson實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05淺談Java數(shù)組的一些使用方法及堆棧存儲(chǔ)
下面小編就為大家?guī)硪黄獪\談Java數(shù)組的一些使用方法及堆棧存儲(chǔ)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-07-07利用Spring IOC技術(shù)實(shí)現(xiàn)用戶登錄驗(yàn)證機(jī)制
這篇文章主要為大家詳細(xì)介紹了Spring IOC技術(shù)實(shí)現(xiàn)用戶登錄驗(yàn)證機(jī)制的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10SpringBoot實(shí)現(xiàn)OneDrive文件上傳的詳細(xì)步驟
這篇文章主要介紹了SpringBoot實(shí)現(xiàn)OneDrive文件上傳的詳細(xì)步驟,文中通過代碼示例和圖文講解的非常詳細(xì),對(duì)大家實(shí)現(xiàn)OneDrive文件上傳有一定的幫助,需要的朋友可以參考下2024-02-02