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