【多线程高并发】深入浅出JMM-Java线程内存模型
實際上JMM是虛擬機根據計算機內存模型模擬而來的,所以說理解計算機內存模型也是很重要的。
為什么Java要引入JMM這個概念?
由于Java是一門跨平臺的語言,所以要屏蔽系統間的差異性;不同系統之間CPU和主存之間的交互速度是有差異的。所以JMM就應運而生了。【舉例來說有可能Linux系統中CPU和主存速度比達到1000:1,Mac系統CPU和主存速度比達到1500:1;所以不可能設計多個規范,苦的還是我們】
在早期,CPU執行指令的速度和主存的存取速度是差不多的;經過不斷地更新和迭代,CPU執行指令的速度已經遠遠超過了主存的存取速度;這時就誕生了一個問題,CPU執行讀寫指令之后還要在那等著主存,即浪費了CPU的運算單元。
🌀注意:高速緩存包含了CPU中的寄存器,以及三級緩存等。
內存包含了RAM和ROM(隨機存取存儲器和只讀存儲器)
那么是如何解決CPU處理器和主存的速度矛盾的呢?
加入了一層讀寫速度盡可能接近處理器速度的高速緩存來作為處理器和主存之間的緩沖。當需要寫入某個數據到主存時,可以直接寫入到緩存,然后在運算結束后,將數據刷新到主存。當需要某個數據時直接從緩存中取即可。
如今大部分的計算機都引入了三級緩存機制,即L1, L2, L3
越往下,容量越大,也就意味著速度越來越慢。
引入了高速緩存會不會出現什么問題呢?
如今的計算機一般都是多核CPU,每個處理器都有自己的高速緩存,但它們又共享同一主存;所以當它們同時對某個變量進行修改時會出現緩存一致性問題。
當某個處理器1正在使用X=1這個變量,然后處理器2將這個X=1修改為了X=2;然后處理器1還在使用舊值。這就出現很大的問題了。
除了上述緩存一致性問題之外,還有一種問題。
為了能夠充分利用CPU的運算單元,引入了指令重排這個概念。也就是CPU按照某種規則對執行的指令進行重新排序以達到最快的速度執行完畢。
看下面的兩條指令
有可能CPU會對這兩個指令進行重排,重排之后結果如下
為什么會這樣?
為了最大可能的利用運算單元,提高性能。符合某種規范進行重排。
JMM內存模型
在多核處理器機器中,多個線程可能會同時執行;當需要intFlag變量時,會拷貝一份副本到自己的工作內存中。每個線程都有各自的工作內存,互不影響。
那么主存和工作內存是怎么交互的呢?
這涉及到了8個原子操作
lock :將變量標記為線程獨有狀態
read:將變量從主內存中讀取出來
load:將read的值放到工作內存的變量副本中
use:將工作內存中的值傳遞給執行引擎
assign:將執行引擎操作結果賦值給工作內存中的變量
store:將工作內存中的變量傳送到主內存中
write:將store的值賦值給主內存中的變量
unlock:解除變量的線程獨有狀態
Java內存模型與計算機內存模型之間的關系
可以把Java中的主存同RAM(隨機存取存儲器)對應起來,將工作內存類比Cache或寄存器。
當線程需要某個數據時,處理器首先去寄存器尋找,如果不存嘗試讀Cache,最后才是RAM【注意,程序運行時一般會將ROM相關的程序數據讀取到RAM中】
注意如今的Cache一般都是三級緩存,即L1,L2,L3
你會不會有這樣的疑問:CPU第一次讀取數據時怎么會命中寄存器或者Cache呢?
每個線程的工作內存會預先把需要的數據復制到Cache和寄存器中,但是不能保證所有的工作內存的變量副本都在Cache中,也有可能在RAM中,具體要看JVM的是如何實現的。
通過JMM演示多線程的可見性問題
public class Visibility {private static boolean initFlag= true;public static void main(String[] args) throws InterruptedException {new Thread(() -> {while (initFlag) {}}).start();TimeUnit.SECONDS.sleep(1);new Thread(() -> {initFlag= false;System.out.println(Thread.currentThread().getName() + "線程將initFlag修改為了false!");}).start();} }你會發現程序一直處于運行中,這是什么原因呢?
通過JMM進行分析
當線程2對initFlag做修改之后并同步到主內存中,但是線程1毫不知情。所以線程1就一直在那做while死循環。
如何解決線程間的可見性問題呢?
使用volatile關鍵字修飾即可。【volatitle能夠保證線程間共享變量的可見性】
那么它的底層是怎么實現的呢?
主要是通過MESI緩存一致性協議來實現的;當線程2對initFlag修改為true之后,處理器2會立即將修改后的數據從緩存中同步到主內存,在這個同步的過程中會經過總線,會被處理器1的總線嗅探機制監聽到initFlag發生了變化,處理器1會立即將緩存的initFlag=false失效。等到下次再使用initFlag時,需要重新從主內存讀取。這樣就保證了線程間共享變量的可見性。
對以上代碼進行反匯編處理,查看底層volatile的實現原理
教你幾步將Java代碼生成匯編指令:https://blog.csdn.net/Kevinnsm/article/details/121695215?spm=1001.2014.3001.5502
Ctrl+F搜索lock關鍵字
可以看出第十五行代碼在匯編指令中使用lock指令前綴
private static volatile boolean flag = true;
繼續向下搜索lock出現的地方
可以看出第25行代碼在底層匯編也使用了lock指令前綴
將flag修改為了false,使用lock前綴之后,CPU會將數據立即刷新到主存中,然后其他CPU根據總線嗅探機制,檢測到flag值發生了變化,會立即將工作內存中的flag失效(因為同步到主內存需要經過總線)【多核計算機中,會有多個處理器,我這里說的CPU不是同一個】
總結
以上是生活随笔為你收集整理的【多线程高并发】深入浅出JMM-Java线程内存模型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【多线程高并发】查看Java代码对应的汇
- 下一篇: 420一个像素多少个字节_一个Java方