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