CPU、內(nèi)存、緩存的關(guān)系
要理解JMM,要先從計算機底層開始,下面是一份大佬的研究報告
計算機在做一些我們平時的基本操作時,需要的響應(yīng)時間是不一樣的!如果我們計算一次a+b所需要的的時間:
- CPU讀取內(nèi)存獲得a,100納秒
- CPU讀取內(nèi)存獲得b,100納秒
- CPU執(zhí)行一條指令 a+b ,0.6納秒
也就是說99%的時間花費在CPU讀取內(nèi)存上了,那如何解決速度不均衡問題?
早期計算機中cpu和內(nèi)存的速度是差不多的,但在現(xiàn)代計算機中cpu的指令速度遠超內(nèi)存的存取速度,由于計算機的存儲設(shè)備與處理器的運算速度有幾個數(shù)量級的差距,所以現(xiàn)代計算機系統(tǒng)都不得不加入一層讀寫速度盡可能接近處理器運算速度的高速緩存(Cache)來作為內(nèi)存與處理器之間的緩沖:將運算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運算能快速進行,當(dāng)運算結(jié)束后再從緩存同步回內(nèi)存之中,這樣處理器就無須等待緩慢的內(nèi)存讀寫了
CPU緩存
什么是CPU緩存
在計算機系統(tǒng)中,CPU高速緩存(英語:CPU Cache,在本文中簡稱緩存)是用于減少處理器訪問內(nèi)存所需平均時間的部件。在金字塔式存儲體系中它位于自頂向下的第二層,僅次于CPU寄存器。其容量遠小于內(nèi)存,但速度卻可以接近處理器的頻率。當(dāng)處理器發(fā)出內(nèi)存訪問請求時,會先查看緩存內(nèi)是否有請求數(shù)據(jù)。如果存在(命中),則不經(jīng)訪問內(nèi)存直接返回該數(shù)據(jù);如果不存在(失效),則要先把內(nèi)存中的相應(yīng)數(shù)據(jù)載入緩存,再將其返回處理器。
下圖是一個典型的存儲器層次結(jié)構(gòu),我們可以看到一共使用了三級緩存:
為什么要有多級CPU Cache
在計算機系統(tǒng)中,寄存器劃是L0級緩存,接著依次是L1,L2,L3(接下來是內(nèi)存,本地磁盤,遠程存儲)。越往上的緩存存儲空間越小,速度越快,成本也更高;越往下的存儲空間越大,速度更慢,成本也更低。從上至下,每一層都可以看做是更下一層的緩存,即:L0寄存器是L1一級緩存的緩存,L1是L2的緩存,依次類推;每一層的數(shù)據(jù)都是來至它的下一層,所以每一層的數(shù)據(jù)是下一層的數(shù)據(jù)的子集
下圖是我電腦的三級緩存,可以看到層級越小容量越小。速度越快價格越高!!
在現(xiàn)代CPU上,一般來說L0, L1,L2,L3都集成在CPU內(nèi)部,而L1還分為一級數(shù)據(jù)緩存(Data Cache,D-Cache,L1d)和一級指令緩存(Instruction Cache,I-Cache,L1i),分別用于存放數(shù)據(jù)和執(zhí)行數(shù)據(jù)的指令解碼。每個核心擁有獨立的運算處理單元、控制器、寄存器、L1、L2緩存,然后一個CPU的多個核心共享最后一層CPU緩存L3。
為了充分利用 CPU Cache,Java提出了內(nèi)存模型這個概念
Java內(nèi)存模型(Java Memory Model,JMM)
從抽象的角度來看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存(Main Memory)中,每個線程都有一個私有的本地內(nèi)存(Local Memory),本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本。本地內(nèi)存是JMM的一個抽象概念,并不真實存在。它涵蓋了緩存、寫緩沖區(qū)、寄存器以及其他的硬件和編譯器優(yōu)化。
程之間的共享變量存儲在主內(nèi)存(Main Memory)中,每個線程都有一個私有的工作內(nèi)存(Local Memory),工作內(nèi)存中存儲了該線程以讀/寫共享變量的副本。
舉個栗子:多個線程去修改主內(nèi)存中的變量a。線程不能直接修改主內(nèi)存中的數(shù)據(jù),先把數(shù)據(jù)拷貝到工作內(nèi)存,線程對私有的工作內(nèi)存修改然后再同步到主內(nèi)存。那這樣做會帶來什么問題呢?
JMM導(dǎo)致的并發(fā)安全問題
從JMM角度看,如果兩個線程同時調(diào)用 a=a+1這個函數(shù)(假設(shè)a的初始值是0),A、B線程同時從主內(nèi)存中拷貝a=0,然后修改寫回,最后主內(nèi)存為a=1,咋搞?
如下是代碼栗子
public class MainTest { private long count = 0; public void incCount() { count += 1; } public static void main(String[] args) throws InterruptedException { MainTest test = new MainTest(); Count count = new Count(test); Count count1 = new Count(test); count.start(); count1.start(); Thread.sleep(5); System.out.println("result is :" + test.count); } private static class Count extends Thread{ private MainTest m; public Count(MainTest m){ this.m = m; } @Override public void run() { for (int i = 0; i < 10000; i++) { m.incCount(); } } } }
執(zhí)行結(jié)果
// 第一次執(zhí)行
> Task :lib-test:MainTest.main()
result is :11861// 第二次執(zhí)行
> Task :lib-test:MainTest.main()
result is :10535
可見性
可見性是指當(dāng)多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值
由于線程對變量的所有操作都必須在工作內(nèi)存中進行,而不能直接讀寫主內(nèi)存中的變量,那么對于共享變量a,它們首先是在自己的工作內(nèi)存,之后再同步到主內(nèi)存。可是并不會及時的刷到主存中,而是會有一定時間差。很明顯,這個時候線程 A 對變量 a 的操作對于線程 B 而言就不具備可見性了 。
要解決共享對象可見性這個問題,我們可以使用volatile關(guān)鍵字或者是加鎖
原子性
即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行
我們都知道CPU資源的分配都是以線程為單位的,并且是分時調(diào)用,操作系統(tǒng)允許某個進程執(zhí)行一小段時間,例如 50 毫秒,過了 50 毫秒操作系統(tǒng)就會重新選擇一個進程來執(zhí)行(我們稱為“任務(wù)切換”),這個 50 毫秒稱為“時間片”。而任務(wù)的切換大多數(shù)是在時間片段結(jié)束以后。
那么線程切換為什么會帶來bug呢?因為操作系統(tǒng)做任務(wù)切換,可以發(fā)生在任何一條CPU 指令執(zhí)行完!注意,是 CPU 指令,CPU 指令,CPU 指令,而不是高級語言里的一條語句。比如count++,在java里就是一句話,但高級語言里一條語句往往需要多條 CPU 指令完成。其實count++包含了三個CPU指令
有序性
即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
在Java內(nèi)存模型中,為了效率是允許編譯器和處理器對指令進行重排序,當(dāng)然重排序不會影響單線程的運行結(jié)果,但是對多線程會有影響。Java提供volatile來保證一定的有序性。最著名的例子就是單例模式里面的DCL(雙重檢查鎖)。另外,可以通過synchronized和Lock來保證有序性,synchronized和Lock保證每個時刻是有一個線程執(zhí)行同步代碼,相當(dāng)于是讓線程順序執(zhí)行同步代碼,自然就保證了有序性。
在單線程的情況下,CPU執(zhí)行語句并不是按照順序來的,為了更高的執(zhí)行效率可能會重新排序,單線程下是可以提高執(zhí)行效率且保證正確。但在多線程下反而變成了安全問題,Java提供volatile來保證一定的有序性。此處不做深入!
volatile
volatile特性
- 可見性:對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入
- 原子性:對任意單個volatile變量的讀/寫具有原子性,但類似于volatile++這種復(fù)合操作不具有原子性
【面試題】為什么volatile不能保證a++的線程安全問題
:線程執(zhí)行a++要經(jīng)歷讀取主內(nèi)存-加載-使用-賦值-寫內(nèi)存-寫回主內(nèi)存幾個階段,而且a++不是原子操作,至少可以分為三步執(zhí)行。線程A、B同時從主內(nèi)存讀取a的值,A線程執(zhí)行到加載階段切換上下文交出CPU使用權(quán),B線程完成整個操作并刷新了主內(nèi)存中a的值。此時A線程繼續(xù)賦值等其他操作,已經(jīng)造成了安全問題。可見性是保證線程每次讀取時必須讀取主內(nèi)存的值,對后續(xù)的操作沒有限制,不會因為主內(nèi)存中的值改變而中斷了操作。如果是原子性則可以,synchronized可以保證原子性。
volatile 的實現(xiàn)原理
有volatile修飾的共享變量進行寫操作的時候會使用CPU提供的Lock前綴指令
- 將當(dāng)前處理器緩存的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存
- 這個寫回內(nèi)存的操作會使其他CPU里緩存了該地址的數(shù)據(jù)無效
單例模式的雙重鎖為什么要加volatile
public class TestInstance{ private volatile static TestInstance instance; public static TestInstance getInstance(){ //1 if(instance == null){ //2 synchronized(TestInstance.class){ //3 if(instance == null){ //4 instance = new TestInstance(); //5 } } } return instance; //6 } }
需要volatile關(guān)鍵字的原因是,在并發(fā)情況下,如果沒有volatile關(guān)鍵字,在第5行會出現(xiàn)問題。
instance = new TestInstance()可以分解為3行偽代碼
a. memory = allocate() //分配內(nèi)存 b. ctorInstanc(memory) //初始化對象 c. instance = memory //設(shè)置instance指向剛分配的地址
上面的代碼在編譯運行時,可能會出現(xiàn)重排序從a-b-c排序為a-c-b。在多線程的情況下會出現(xiàn)以下問題。當(dāng)線程A在執(zhí)行第5行代碼時,B線程進來執(zhí)行到第2行代碼。假設(shè)此時A執(zhí)行的過程中發(fā)生了指令重排序,即先執(zhí)行了a和c,沒有執(zhí)行b。那么由于A線程執(zhí)行了c導(dǎo)致instance指向了一段地址,所以B線程判斷instance不為null,會直接跳到第6行并返回一個未初始化的對象
總結(jié)
因為CPU與內(nèi)存的速度差距越來越大,為了彌補速度差距引入了CPU緩存,又因為緩存導(dǎo)致線程安全問題,從前到后縷出一條線來就很容易理解了。如果只是單線程完全不擔(dān)心什么指令重排,想要更高的執(zhí)行效率必然付出安全風(fēng)險。知其然,知其所以然!
到此這篇關(guān)于java 多線程與并發(fā)之volatile詳解分析的文章就介紹到這了,更多相關(guān)Java volatile內(nèi)容請搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!
原文鏈接:https://blog.csdn.net/xihuailu3244/article/details/115454622