在學(xué)習Java內(nèi)存模型之前牙瓢,有幾個知識點必須先了解一下。
1. cpu和物理內(nèi)存的讀寫速度差會導(dǎo)致什么問題辜妓?如何解決?
2. 計算機內(nèi)存模型是什么忌怎?為什么需要計算機內(nèi)存模型籍滴?
最后再了解:
3. 什么是JMM?
cpu和物理內(nèi)存的讀寫速度差會導(dǎo)致什么問題榴啸?如何解決?
計算機的每條指令孽惰,都是通過cpu來執(zhí)行的,執(zhí)行過程中鸥印,大多情況下都需要與內(nèi)存打交道勋功。在早些時候,cpu的運算速度和內(nèi)存的運算速度相差無幾库说,于是兩者過的相安無事狂鞋。但是隨著cpu的不斷發(fā)展,cpu的計算速度遠遠超過了內(nèi)存璃弄。所以就導(dǎo)致每次cpu都要在等待內(nèi)存上耗費很長時間要销。
于是就誕生了高速緩存這一中間人构回,其實也就是我們平常所說的緩存夏块。它的作用時:在cpu進行計算時疏咐,先將所需的數(shù)據(jù)從內(nèi)存拷貝一份到高速緩存中,cpu在獲取數(shù)據(jù)時脐供,就直接從緩存中獲取浑塞,寫數(shù)據(jù)的時候,也可以直接往高速緩存中寫入政己。運算結(jié)束時酌壕,再將緩存中的數(shù)據(jù)刷新到內(nèi)存中即可。
舉個例子歇由,有一份工作需要張三和李四配合進行卵牍,張三負責工作前半部分,李四負責工作后半部分沦泌,起初兩者都是新手糊昙,干活速度不相上下,于是這個工作剛好能銜接上谢谦。但是隨著時間的積累释牺,李四的干活速度越來越快(cpu在升級),而張三的干活速度依舊如此(內(nèi)存的速度在原地踏步)回挽,所以導(dǎo)致李四干完了没咙,需要浪費很多時間在等待張三上。 所以呢千劈,公司為了這個問題祭刚,又招了王五作為協(xié)調(diào)員,又招了n個張三來做底層工作墙牌。協(xié)調(diào)員這邊負責將n個張三完成的工作緩存起來(高速緩存的作用)袁梗,等到李四開始工作時,直接與協(xié)調(diào)員對接即可憔古。
計算機內(nèi)存模型是什么遮怜?為什么需要計算機內(nèi)存模型?
咱們這先提一下計算機緩存鸿市,計算機緩存分一級緩存(L1 cache)锯梁、二級緩存(L2 cache)、三級緩存(L3 cache)焰情。當然不是所有計算機都配有這三級陌凳,也是看配置的。
在cpu執(zhí)行時内舟,會先從一級緩存中獲取數(shù)據(jù)合敦,如果沒有再從二級緩存中獲取數(shù)據(jù),如果還是沒有验游,則會去三級緩存或者內(nèi)存中獲取數(shù)據(jù)充岛。
對于多核cpu來說保檐,每個核都會有自己的一級緩存,二級緩存可能是共享的崔梗,也可能是單獨的夜只,而三級緩存一般來說都是共享的。
說完了計算機緩存蒜魄,咱們再聊一下計算機的單線程和多線程扔亥,單核與多核的問題,這將對計算機內(nèi)存模型產(chǎn)生至關(guān)重要的影響谈为。
我們可以分下面幾種情況來考慮:
單線程(無論單核還是多核)
單線程執(zhí)行旅挤,就不用考慮資源競爭的問題了,反正數(shù)據(jù)都是按順序讀的伞鲫,按順序?qū)懙那澹粫嬖诓l(fā)修改等并發(fā)問題,這種時候也就不需要對內(nèi)存數(shù)據(jù)進行什么保護措施榔昔,數(shù)據(jù)是絕對不可能亂的驹闰。單核多線程
單核是可以開啟多線程操作的,但不要誤以為這是并行撒会,你肉眼看著多個任務(wù)同時進行嘹朗,其實底層操作系統(tǒng)還是按著串行但方式在運行,操作系統(tǒng)是以時間片為單位控制著每個線程的掛起和執(zhí)行诵肛。
舉例來說屹培,比如當下有a、b怔檩、c三個線程褪秀,操作系統(tǒng)給a分了10個時間片,于是a執(zhí)行了10個時間片后薛训,就被操作系統(tǒng)掛起了媒吗,緊接著又給b分了10個時間片,b執(zhí)行完后也被掛起了乙埃,如此反復(fù)闸英,因為一個時間片大概10ms左右,所以以人肉眼的角度來觀察多個任務(wù)的執(zhí)行情況介袜,其實是看不出這其實是在串行執(zhí)行甫何。
所以,這也解釋了遇伞,這種情況下辙喂,其實也不需要對內(nèi)存數(shù)據(jù)做什么保障,串行已經(jīng)解決了一切臟數(shù)據(jù)問題。多核多線程
上面也說到了巍耗,每個核心秋麸,是有自己對一級緩存的,所以當core1上當線程1修改了該內(nèi)核下當L1緩存后芍锦,core2上當線程2修改了該內(nèi)核下當L1緩存,這時兩者當緩存數(shù)據(jù)就對不上了飞盆,如果這時候需要對這份數(shù)據(jù)進行其它計算娄琉,那就會導(dǎo)致計算錯誤。這其實也就是我們經(jīng)常說的緩存一致性問題吓歇。
除了上述多核多線程的情況會導(dǎo)致緩存不一致問題之外孽水,如果考慮硬件方面的話,還有其它情況城看,我這里就說一點:
- 指令重排序
重排序是指編譯器和處理器為了優(yōu)化程序性能而對指令序列進行重新排序的一種手段女气。
假設(shè)現(xiàn)在有如下兩句代碼要執(zhí)行:
a = 1; a = 2;
很明顯,先執(zhí)行a=1
或a=2
测柠,對于a的結(jié)果值來說是顯然不同的兩種情況炼鞠。當然,在單線程情況下轰胁,操作系統(tǒng)會在遵守原有的串行邏輯的語義下谒主,對指令進行重排序,人話來說就是:我不會讓你對結(jié)果變的赃阀!
但不同處理器和不同線程之間的數(shù)據(jù)性不會被編譯器和處理器考慮到霎肯,所以會因此而產(chǎn)生數(shù)據(jù)不一致的情況。
那這些問題操作系統(tǒng)怎么解決呢榛斯?內(nèi)存模型观游!
簡單的來說,計算機內(nèi)存模型定義了兩種解決方式:
- 內(nèi)存屏障
- 限制處理器做優(yōu)化
什么是JMM驮俗?
- Java內(nèi)存模型是一個抽象的概念懂缕,并非真實存在。
- JMM定義了線程和主內(nèi)存之間的關(guān)系:
- 線程之間的共享變量存儲在主內(nèi)存中王凑。
- 每個線程都有一個私有的本地內(nèi)存提佣,其中保存了該線程使用到的共享變量的主內(nèi)存副本拷貝。
- JMM屏蔽了操作系統(tǒng)和硬件的內(nèi)存訪問差異荤崇,所以Java才得意在各個平臺上達到訪問內(nèi)存的一致性拌屏。
- 重點:主內(nèi)存和線程私有的工作內(nèi)存,與JVM的堆棧不是一回事术荤。主內(nèi)存應(yīng)該是指內(nèi)存條倚喂,而工作內(nèi)存應(yīng)該是指寄存器和高速緩存。
JMM的核心原則
多線程的原子性、可見性端圈、有序性焦读。
JMM是一種規(guī)范,目的是解決由于多線程通過共享內(nèi)存進行通信時舱权,存在的本地內(nèi)存數(shù)據(jù)不一致矗晃、編譯器會對代碼指令重排序、處理器會對代碼亂序執(zhí)行等帶來的問題宴倍。目的是保證并發(fā)編程場景中的原子性张症、可見性和有序性。
原子性
- 原子性是指一個操作是不可中斷的鸵贬,即多線程環(huán)境下俗他,操作不能被其他線程干擾
可見性
- 可見性是指當一個線程修改了某一個共享變量的值,其他線程是否能夠立即知道該變更
- Java中普通的共享變量不保證可見性阔逼,因為其修改被寫入內(nèi)存的時機是不確定的兆衅,多線程并發(fā)下很可能出現(xiàn)"臟讀"
- 緩存優(yōu)化或者硬件優(yōu)化或指令重排以及編輯器的優(yōu)化都可能導(dǎo)致一個線程修改不會立即被其他線程察覺
- Java提供volatile保證可見性:寫操作立即刷新到主內(nèi)存,讀操作直接從主內(nèi)存讀取
- Java同時還可以通過加鎖的同步性間接保證可見性:synchronized和Lock能保證同一時刻只有一個線程獲取鎖并執(zhí)行同步代碼嗜浮,并在釋放鎖之后將變量的修改刷新到主內(nèi)存中
有序性
- 對于一個線程的執(zhí)行代碼而言羡亩,我們總是習慣性認為代碼的執(zhí)行總是從上到下,有序執(zhí)行危融,但為了提供性能夕春,編譯器和處理器通常會對指令序列進行重新排序
- 指令重排可以保證串行語義一致,但沒有義務(wù)保證多線程間的語義也一致专挪,即可能產(chǎn)生"臟讀"
參考文章:
https://mp.weixin.qq.com/s/n0U2IJwhT3OAp_EwRdzYIA
https://www.zybuluo.com/kiraSally/note/850631#3happends-before