高并发编程-重新认识Java内存模型(JMM)
文章目錄
- 從CPU到內(nèi)存模型
- 內(nèi)存模型如何確保緩存一致性
- 并發(fā)變成需要解決的問題 (原子性、可見性、有序性)
- 內(nèi)存模型需要解決的問題
- Java內(nèi)存模型
- JMM的API實(shí)現(xiàn)
- 原子性 synchronized
- 可見性 volatile 、 synchronized 、 final
- 有序性 synchronized 、volatile
從CPU到內(nèi)存模型
高并發(fā)編程-通過volatile重新認(rèn)識(shí)CPU緩存 和 Java內(nèi)存模型(JMM)
說到j(luò)ava內(nèi)存模型, 我們先探討下 內(nèi)存模型(Memory Model) , 內(nèi)存模型是和計(jì)算機(jī)硬件相關(guān)的一個(gè)概念。
先簡(jiǎn)單來了解下 計(jì)算機(jī)內(nèi)存模型,然后再來引出 Java內(nèi)存模型和計(jì)算機(jī)內(nèi)存模型的關(guān)聯(lián)關(guān)系。
計(jì)算機(jī)在執(zhí)行程序的時(shí)候,每條指令都是在CPU中執(zhí)行的,而執(zhí)行的時(shí)候,不可避免的要和數(shù)據(jù)進(jìn)行交互。 數(shù)據(jù)存在哪里呢 ?--------->存放在主存當(dāng)中的,即計(jì)算機(jī)的物理內(nèi)存。 (存在內(nèi)存中對(duì)于提高計(jì)算機(jī)的執(zhí)行效率必不可少)
最開始, CPU和內(nèi)存相安無事,內(nèi)存的速度還能匹配的上CPU的運(yùn)行速度。 隨著CPU技術(shù)的發(fā)展,CPU的執(zhí)行速度越來越快。但內(nèi)存的技術(shù)并沒有質(zhì)的提高,所以從內(nèi)存中讀取和寫入數(shù)據(jù)的過程和CPU的執(zhí)行速度比起來差距越來越大,導(dǎo)致CPU每次操作內(nèi)存都要耗時(shí)很長(zhǎng)。
所以為了解決這個(gè)問題,引入了高速緩存
所以程序的執(zhí)行過程變?yōu)?#xff1a;
隨著CPU能力的不斷提升, CPU的運(yùn)算速度超越了1級(jí)緩存的數(shù)據(jù)I\O能力,CPU廠商又引入了多級(jí)的緩存結(jié)構(gòu)。
按照數(shù)據(jù)讀取順序和與CPU結(jié)合的緊密程度,CPU緩存可以分為一級(jí)緩存(L1),二級(jí)緩存(L3),部分高端CPU還具有三級(jí)緩存(L3),每一級(jí)緩存中所儲(chǔ)存的全部數(shù)據(jù)都是下一級(jí)緩存的一部分。
在有了多級(jí)緩存之后,程序的執(zhí)行就變成了:
當(dāng)CPU要讀取一個(gè)數(shù)據(jù)時(shí),首先從一級(jí)緩存中查找,如果沒有找到再?gòu)亩?jí)緩存中查找,如果還是沒有就從三級(jí)緩存或內(nèi)存中查找。
單核CPU只含有一套L1,L2,L3緩存;
多核CPU,則每個(gè)核心都含有一套L1(甚至和L2)緩存,而 共享L3(或者和L2)緩存。
- L1是最接近CPU的,它容量最小、例如32K、速度最快,每個(gè)核上都有一個(gè)L1 Cache(準(zhǔn)確地說每個(gè)核上有兩個(gè)L1
Cache,一個(gè)存數(shù)據(jù) L1d Cache,一個(gè)存指令 L1i Cache)。 - L2 Cache 容量更大一些、例如256K、速度要稍慢一些,一般情況下每個(gè)核上都有一個(gè)獨(dú)立的L2 Cache;
- L3 Cache是三級(jí)緩存中最大的一級(jí)、例如12MB、同時(shí)也是最慢的一級(jí),在同一個(gè)CPU插槽之間的核共享一個(gè)L3 Cache。
為了高效地存取緩存,不是簡(jiǎn)單隨意地將單條數(shù)據(jù)寫入緩存的。
緩存是由緩存行組成的,典型的一行是64字節(jié)。CPU存取緩存都是按行為最小單位操作的。一個(gè)Java long型占8字節(jié),所以從一條緩存行上可以獲取到8個(gè)long型變量。那么如果要訪問一個(gè)long類型數(shù)組,當(dāng)有一個(gè)long元素對(duì)象被加載到cache中,將會(huì)無消耗地加載了另外7個(gè),所以可以非??斓乇闅v數(shù)組。
由于多核是可以并行的,可能會(huì)出現(xiàn)多個(gè)線程同時(shí)寫各自的緩存的情況,而各自的cache之間的數(shù)據(jù)就有可能不同。
還有一種硬件問題 : 為了使處理器內(nèi)部的運(yùn)算單元能夠盡量的被充分利用,處理器可能會(huì)對(duì)輸入代碼進(jìn)行亂序執(zhí)行處理。這就是處理器優(yōu)化。
除了處理器會(huì)對(duì)代碼進(jìn)行優(yōu)化亂序處理,編程語言的編譯器也會(huì)有類似的優(yōu)化,比如Java虛擬機(jī)的即時(shí)編譯器(JIT)也會(huì)做指令重排。
可想而知,如果任由處理器優(yōu)化和編譯器對(duì)指令重排的話,就可能導(dǎo)致各種各樣的問題。
如何解決呢? 為了解決這個(gè)問題 ------------------------> 引入了 內(nèi)存模型
內(nèi)存模型如何確保緩存一致性
內(nèi)存模型到底是怎么保證緩存一致性的呢 ,通常有如下了兩種方案
1、通過在總線加LOCK#鎖的方式
2、通過緩存一致性協(xié)議(Cache Coherence Protocol)
早期的CPU,通過在總線上加LOCK#鎖的形式來解決緩存不一致的問題,但是在鎖住總線期間,其他CPU無法訪問內(nèi)存,會(huì)導(dǎo)致效率低下。 所以引入了第二種 緩存一致性協(xié)議(Cache Coherence Protocol)。
緩存一致性協(xié)議 , 最出名的就是Intel 的MESI協(xié)議,MESI協(xié)議保證了每個(gè)緩存中使用的共享變量的副本是一致的。
MESI的核心的思想:當(dāng)CPU寫數(shù)據(jù)時(shí),如果發(fā)現(xiàn)操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會(huì)發(fā)出信號(hào)通知其他CPU將該變量的緩存行置為無效狀態(tài),因此當(dāng)其他CPU需要讀取這個(gè)變量時(shí),發(fā)現(xiàn)自己緩存中緩存該變量的緩存行是無效的,那么它就會(huì)從內(nèi)存重新讀取。
在MESI協(xié)議中,每個(gè)緩存可能有有4個(gè)狀態(tài),它們分別是:
- M(Modified):這行數(shù)據(jù)有效,數(shù)據(jù)被修改了,和內(nèi)存中的數(shù)據(jù)不一致,數(shù)據(jù)只存在于本Cache中。
- E(Exclusive):這行數(shù)據(jù)有效,數(shù)據(jù)和內(nèi)存中的數(shù)據(jù)一致,數(shù)據(jù)只存在于本Cache中。
- S(Shared):這行數(shù)據(jù)有效,數(shù)據(jù)和內(nèi)存中的數(shù)據(jù)一致,數(shù)據(jù)存在于很多Cache中。
- I(Invalid):這行數(shù)據(jù)無效。
MESI協(xié)議,可以保證緩存的一致性,但是無法保證實(shí)時(shí)性。
并發(fā)變成需要解決的問題 (原子性、可見性、有序性)
原子性是指在一個(gè)操作中就是cpu不可以在中途暫停然后再調(diào)度,既不被中斷操作,要不執(zhí)行完成,要不就不執(zhí)行。
可見性是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。
有序性即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
結(jié)合上面所說的,可以理解為: 緩存一致性問題—>可見性問題。處理器優(yōu)化會(huì)導(dǎo)致原子性問題的。指令重排即會(huì)導(dǎo)致有序性問題
內(nèi)存模型需要解決的問題
為了保證共享內(nèi)存的正確性(可見性、有序性、原子性),內(nèi)存模型定義了共享內(nèi)存系統(tǒng)中多線程程序讀寫操作行為的規(guī)范。通過這些規(guī)則來規(guī)范對(duì)內(nèi)存的讀寫操作,從而保證指令執(zhí)行的正確性。它與處理器有關(guān)、與緩存有關(guān)、與并發(fā)有關(guān)、與編譯器也有關(guān)。他解決了CPU多級(jí)緩存、處理器優(yōu)化、指令重排等導(dǎo)致的內(nèi)存訪問問題,保證了并發(fā)場(chǎng)景下的一致性、原子性和有序性。
內(nèi)存模型解決并發(fā)問題主要采用兩種方式:限制處理器優(yōu)化和使用內(nèi)存屏障。
Java內(nèi)存模型
計(jì)算機(jī)內(nèi)存模型,這是解決多線程場(chǎng)景下并發(fā)問題的一個(gè)重要規(guī)范。那么實(shí)現(xiàn)上,不同的編程語言,可能有所不同。
Java內(nèi)存模型(Java Memory Model ,JMM)就是一種符合內(nèi)存模型規(guī)范的,屏蔽了各種硬件和操作系統(tǒng)的訪問差異的,保證了Java程序在各種平臺(tái)下對(duì)內(nèi)存的訪問都能保證效果一致的機(jī)制及規(guī)范。
我們這里指的是 JDK 5 開始使用的新的內(nèi)存模型JSR-133: JavaTM Memory Model and Thread Specification
Java內(nèi)存模型規(guī)定了所有的共享變量都存儲(chǔ)在主內(nèi)存中,每條線程還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了該線程中是用到的變量的主內(nèi)存副本拷貝,線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存。不同的線程之間也無法直接訪問對(duì)方工作內(nèi)存中的變量,線程間變量的傳遞均需要自己的工作內(nèi)存和主存之間進(jìn)行數(shù)據(jù)同步進(jìn)行。
JMM就作用于工作內(nèi)存和主存之間數(shù)據(jù)同步過程。JMM規(guī)定了如何做數(shù)據(jù)同步以及什么時(shí)候做數(shù)據(jù)同步。
故: JMM是一種規(guī)范,目的是解決由于多線程通過共享內(nèi)存進(jìn)行通信時(shí),存在的本地內(nèi)存數(shù)據(jù)不一致、編譯器會(huì)對(duì)代碼指令重排序、處理器會(huì)對(duì)代碼亂序執(zhí)行等帶來的問題。
JMM的API實(shí)現(xiàn)
Java中提供了很多和并發(fā)處理相關(guān)的關(guān)鍵字,比如volatile、synchronized、final、j.u.c包 等 ,這些關(guān)鍵字或者包就是Java內(nèi)存模型封裝了底層實(shí)現(xiàn)后,供開發(fā)者直接使用
原子性 synchronized
在Java中可以使用synchronized來保證方法和代碼塊內(nèi)的操作是原子性的。 為了保證原子性,synchronized提供了兩個(gè)高級(jí)的字節(jié)碼指令monitorenter和monitorexit 。 具體細(xì)節(jié)另外開篇討論。
可見性 volatile 、 synchronized 、 final
Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值的這種依賴主內(nèi)存作為傳遞媒介的方式來實(shí)現(xiàn)的。
Java中的volatile關(guān)鍵字提供了一個(gè)功能,那就是被其修飾的變量在被修改后可以立即同步到主內(nèi)存,被其修飾的變量在每次是用之前都從主內(nèi)存刷新。因此,可以使用volatile來保證多線程操作時(shí)變量的可見性。
除了volatile,Java中的synchronized和final兩個(gè)關(guān)鍵字也可以實(shí)現(xiàn)可見性 。
有序性 synchronized 、volatile
在Java中,可以使用synchronized和volatile來保證多線程之間操作的有序性。實(shí)現(xiàn)方式有所區(qū)別:
volatile關(guān)鍵字會(huì)禁止指令重排。synchronized關(guān)鍵字保證同一時(shí)刻只允許一條線程操作。
總結(jié)
以上是生活随笔為你收集整理的高并发编程-重新认识Java内存模型(JMM)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 高并发编程-通过volatile重新认识
- 下一篇: Java-CentoOS 7安装JDK8