摘要:主機(jī)架構(gòu)與內(nèi)存模型多任務(wù)處理器在現(xiàn)代計(jì)算機(jī)系統(tǒng)中幾乎已是一項(xiàng)必備的功能了。在計(jì)算機(jī)系統(tǒng)中,可能存在多個(gè)處理器,每個(gè)處理器都有自己的高速緩存,而他們又共享同一主內(nèi)存。
計(jì)算機(jī):輔助人腦的好工具
計(jì)算機(jī)的定義:
接受使用者輸入指令與數(shù)據(jù), 經(jīng)由中央處理器的數(shù)學(xué)與邏輯單元運(yùn)算處理后,以產(chǎn)生或儲(chǔ)存成有用的信息
我們的個(gè)人電腦也是計(jì)算機(jī)的一種,,依外觀來看這家伙主要分三部分:
輸入單元:包括鍵盤、鼠標(biāo)、讀卡機(jī)、掃描器、手寫板、觸控螢?zāi)坏鹊纫欢眩?/p>
主機(jī)部分:這個(gè)就是系統(tǒng)單元,被主機(jī)機(jī)殼保護(hù)住了,里面含有 CPU 與主內(nèi)存等;
輸出單元:例如螢?zāi)?、打印機(jī)等等
中央處理器(Central Processing Unit)而我們今天研究的主題就是計(jì)算機(jī)其中的主機(jī)部分。整部主機(jī)的重點(diǎn)在于中央處理器(cpu),cpu是一個(gè)具有特定功能的芯片,
里面含有很多微指令集,計(jì)算機(jī)所有的功能都需要微指令集的支持才可以完成。cpu的主要作用在于管理和運(yùn)算,因此cpu內(nèi)部又可分為兩個(gè)單元,分別為:算數(shù)邏輯單元和控制單元。其中算數(shù)邏輯單元主要負(fù)責(zé)程序運(yùn)算和邏輯判斷,控制單元主要負(fù)責(zé)和各周邊主件與各單元之間的工作。
上圖所展示的系統(tǒng)單元其實(shí)就是主機(jī)的主要組件,其中的核心就是cpu和主內(nèi)存。基本上所有數(shù)據(jù)都要經(jīng)過主內(nèi)存,至于是流入還是流出則是cpu所發(fā)布的控制指令,而cpu實(shí)際要處理的數(shù)據(jù)則全部來自于主內(nèi)存!
cpu的外頻與倍頻
cpu作為計(jì)算機(jī)的大腦,因?yàn)樵S多運(yùn)算和邏輯都在cpu里處理,所以需要其擁有很強(qiáng)大的處理能力,但外部組件的速度和cpu的速度相差實(shí)在太多,才啊有了所謂的外頻和倍頻。
所謂外頻指的是cpu與外部組件進(jìn)行數(shù)據(jù)傳輸?shù)乃俣?。倍頻則是cpu內(nèi)部用來加速工作的一個(gè)倍數(shù)。兩者相乘才是cpu自己的主頻。
高速緩存
程序的啟動(dòng)和運(yùn)轉(zhuǎn)有著一個(gè)重要的問題,即系統(tǒng)花費(fèi)了大量的時(shí)間把信息從一個(gè)地方挪到另一個(gè)地方。數(shù)據(jù)最初放在磁盤上,當(dāng)程序被加載時(shí),將其移動(dòng)到主內(nèi)存,當(dāng)程序運(yùn)行時(shí),指令又從內(nèi)存復(fù)制到cpu上。從程序員的角度來看,這些復(fù)制就是開銷,是減慢了程序運(yùn)行速度的罪魁禍?zhǔn)?。因此,系統(tǒng)設(shè)計(jì)者設(shè)計(jì)了高速緩存來使這些復(fù)制操作盡可能快地完成。
一個(gè)系統(tǒng)上磁盤驅(qū)動(dòng)器可能比主內(nèi)存大100倍,但是對處理器來說,從磁盤驅(qū)動(dòng)器讀取一個(gè)字的開銷比從主內(nèi)存讀取的開銷大1000萬倍。類似的,一個(gè)寄存器只可以儲(chǔ)存幾百字節(jié)的信息,而主內(nèi)存里可以放幾十億字節(jié)。然而寄存器的速度大約是主內(nèi)存的100倍。而且,隨著半導(dǎo)體技術(shù)的進(jìn)步,這種處理器與主存之間的差距還在持續(xù)增大。
針對這種處理器與主存之間的差異,系統(tǒng)設(shè)計(jì)者采用了更小更快的存儲(chǔ)設(shè)備,稱為高速緩存存儲(chǔ)器。其中又分為L1、L2、L3高速緩存,限于篇幅,在這里就不給大家詳細(xì)介紹了.系統(tǒng)通過讓高速緩存里存放可能經(jīng)常訪問的數(shù)據(jù),大部分的內(nèi)存操作都能在快速的高速緩存中完成。
多任務(wù)處理器在現(xiàn)代計(jì)算機(jī)系統(tǒng)中幾乎已是一項(xiàng)必備的功能了。所有的運(yùn)算任務(wù)至少都要與主內(nèi)存交互才能完成,由于計(jì)算機(jī)的存儲(chǔ)設(shè)備和處理器的運(yùn)算速度之間存在著幾個(gè)數(shù)量級的差距。所以現(xiàn)代計(jì)算機(jī)系統(tǒng)都不得不加入一層讀寫速度盡可能接近于處理器的高速緩存來作為內(nèi)存與處理器之間的緩沖:將運(yùn)算需要使用的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算高速進(jìn)行,當(dāng)運(yùn)算結(jié)束后,再將緩存中的結(jié)果復(fù)制到主內(nèi)存中。這樣處理器就不需要等待緩慢的內(nèi)存讀寫了。如下圖所示:
看似很美好,實(shí)際上并沒有想象中的那么容易。在計(jì)算機(jī)系統(tǒng)中,可能存在多個(gè)處理器,每個(gè)處理器都有自己的高速緩存,而他們又共享同一主內(nèi)存。當(dāng)多個(gè)處理器的運(yùn)算任務(wù)涉及到統(tǒng)一塊內(nèi)存區(qū)域,將可能導(dǎo)致高速緩存間的不一致,那同步到主內(nèi)存以哪個(gè)為準(zhǔn)呢?為了解決一致性問題,需要各個(gè)處理器訪問緩存需要遵循一些一致性協(xié)議來進(jìn)行操作。java內(nèi)存模型定義的內(nèi)存訪問操作和硬件的訪問操作是有可比性的。
java虛擬機(jī)規(guī)范試圖定義一種java內(nèi)存模型來屏蔽掉各種硬件和操作系統(tǒng)的訪問差異,以實(shí)現(xiàn)讓java程序在任何機(jī)器上都能達(dá)到一致的相同效果。因此定義java內(nèi)存模型是一件非常麻煩的事,既要足夠嚴(yán)謹(jǐn),讓java的并發(fā)操作不會(huì)發(fā)生歧義;但也必須足夠?qū)捤?,使虛擬機(jī)的實(shí)現(xiàn)有足夠的自由空間去利用硬件的各種特性(寄存器、高速緩存等)來獲取更好的執(zhí)行速度。內(nèi)存模型如下圖所示:
在講重排序之前,我們先來看一段代碼:
public class ReOrderTest { private static int x = 0, y = 0; private static int a = 0, b = 0; public static void main(String[] args) throws InterruptedException { int i = 0; for (; ; ) { i++; x = 0;y = 0;a = 0;b = 0; CountDownLatch latch = new CountDownLatch(1); Thread one = new Thread(() -> { try { latch.await(); } catch (InterruptedException e) { } a = 1; x = b; }); Thread other = new Thread(() -> { try { latch.await(); } catch (InterruptedException e) { } b = 1; y = a; }); one.start();other.start(); latch.countDown(); one.join();other.join(); String result = "第" + i + "次 (" + x + "," + y + ")"; if (x == 0 && y == 0) { System.err.println(result); break; } else { System.out.println(result); } } } }
看完這段代碼,或許沒有接觸過重排序的同學(xué)會(huì)認(rèn)為這是一個(gè)死循環(huán),其輸出結(jié)果只會(huì)有(1,1),(1,0),(0,1)三種結(jié)果。但實(shí)際上只需要運(yùn)行幾秒鐘,就會(huì)break出來,出現(xiàn)x=0;y=0的情況。
重排序由以下幾種機(jī)制引起的:
編譯器優(yōu)化:對于沒有數(shù)據(jù)依賴關(guān)系的操作,編譯器在編譯的過程中會(huì)進(jìn)行一定程度的重排。
解釋:編譯器可以將線程1的a=1和x=b互換下位置的,因?yàn)樗麄儾淮嬖跀?shù)據(jù)依賴,同理線程2也可以互換位置,就 可以得到x=0,y=0的結(jié)果了
指令重排序:CPU 優(yōu)化行為,也是會(huì)對不存在數(shù)據(jù)依賴關(guān)系的指令進(jìn)行一定程度的重排。
解釋:這個(gè)和編譯器優(yōu)化是一個(gè)道理,代碼編譯成指令,不存在依賴關(guān)系也就有可能進(jìn)行重排
內(nèi)存系統(tǒng)重排序:內(nèi)存系統(tǒng)沒有重排序,但是由于有緩存的存在,使得程序整體上會(huì)表現(xiàn)出亂序的行為。
解釋:線程1執(zhí)行a=1,將其寫入緩存但可能還沒有同步到主內(nèi)存,這個(gè)時(shí)候線程2訪問a的值當(dāng)然就是0了。同理線程2對b的賦值操作也有可能沒有刷新到主內(nèi)存當(dāng)中
剛才再講重排序的時(shí)候,就提到了內(nèi)存可見性。線程1執(zhí)行a=1,這個(gè)結(jié)果對于線程2來說不一定可見。這種不可見不是由于多處理器造成的,而是由于多緩存造成的?,F(xiàn)在每個(gè)處理器上都會(huì)有寄存器,L1、L2、L3緩存等等,問題就發(fā)生在每個(gè)處理器都獨(dú)占一個(gè)緩存,數(shù)據(jù)修改刷入緩存,然后從緩存刷入內(nèi)存,所以就會(huì)導(dǎo)致有些處理器讀到的是過期的值。java作為高級語言,為我們抽象jmm模型,定義了讀寫數(shù)據(jù)的規(guī)范,使我們不用關(guān)心緩存的概念,但是jmm也同時(shí)給我們抽象出了工作內(nèi)存和主內(nèi)存。(ps:這里說的工作內(nèi)存是對寄存器,L1、L2、L3緩存等的一個(gè)抽象)
happens-before(先行發(fā)生原則)happens-before是理解jmm最核心的概念。對于java程序員來說,如果你想理解并寫好并發(fā)程序,happens-before是理解jmm模型的關(guān)鍵。
《JSR-133:Java Memory Model and Thread Specification》對happens-before關(guān)系的定義如下:
1)如果一個(gè)操作happens-before另一個(gè)操作,那么第一個(gè)操作的執(zhí)行結(jié)果將對第二個(gè)操作可見,而且第一個(gè)操作的執(zhí)行順序排在第二個(gè)操作之前。
2)兩個(gè)操作之間存在happens-before關(guān)系,并不意味著Java平臺(tái)的具體實(shí)現(xiàn)必須要按照happens-before關(guān)系指定的順序來執(zhí)行。如果重排序之后的執(zhí)行結(jié)果,與按happens-before關(guān)系來執(zhí)行的結(jié)果一致,那么這種重排序并不非法(也就是說,JMM允許這種重排序)。
public static int getz() { int x=1; //A int y=1; //B int z=x+y; //C return z; }
上面的代碼示例存在了3個(gè)happens-before規(guī)范:
Ahappens-beforeB
Bhappens-beforeC
Ahappens-beforeC
其中2、3是必須的,而1不是必需的。因此jmm又把happens-before要求禁止的重排序分為了以下兩種:
會(huì)改變程序結(jié)果的重排序(jmm要求編譯器和處理器嚴(yán)格禁止這種重排序)
不會(huì)改變程序結(jié)果的重排序(允許,指的是單線程程序或者經(jīng)過正確同步的多線程程序)
happens-before規(guī)則
《JSR-133:Java Memory Model and Thread Specification》定義了如下happens-before規(guī)則。
1)程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happens-before于該線程中的任意后續(xù)操作。
2)監(jiān)視器鎖規(guī)則:對一個(gè)鎖的解鎖,happens-before于隨后對這個(gè)鎖的加鎖。
3)volatile變量規(guī)則:對一個(gè)volatile域的寫,happens-before于任意后續(xù)對這個(gè)volatile域的
讀。
4)傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。
5)start()規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動(dòng)線程B),那么A線程的
ThreadB.start()操作happens-before于線程B中的任意操作。
6)join()規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程B中的任意操作
happens-before于線程A從ThreadB.join()操作成功返回。
我們其中最常見的就是1、2、3、4.其中1、4的情況在前面已經(jīng)討論過。3)將會(huì)在volatile的內(nèi)存語義中進(jìn)行討論?,F(xiàn)在我們來看下鎖的釋放-獲取建立的happens-before關(guān)系:
int a=0; public synchronized void read(){//1 a++;//2 }//3 public synchronized void writer(){//4 int i=a+1;//5 }//6
由程序順序規(guī)則來判斷:1happens-before2,2happens-before3,4happens-before5,5happens-before6.
由監(jiān)視器鎖規(guī)則來判斷:3happens-before4
由傳遞性來判斷:1happens-before2,2happens-before3,3happens-before4,4happens-before5,5happens-before6
怎么實(shí)現(xiàn)的呢?進(jìn)入鎖的時(shí)候?qū)?huì)使工作內(nèi)存失效,讀取變量必須從主內(nèi)存中讀取。釋放鎖得時(shí)候會(huì)將該變量刷新回主內(nèi)存。這里的鎖包括conuurent包下的鎖.
關(guān)于volatile,大家只需要牢記兩點(diǎn):內(nèi)存可見和禁止重排序.
關(guān)于volatile的可見性,經(jīng)常被大家誤解。認(rèn)為volatile變量對所有線程是立即可見的,對volatile變量所有的寫操作都能立刻反映到其他縣城中,換句話說,volatile變量的運(yùn)算在并發(fā)下是安全的。這個(gè)結(jié)論是錯(cuò)誤的,雖然volatile變量可以保證可見性,但是java里面的運(yùn)算并非原子操作,導(dǎo)致volatile變量的運(yùn)算在并發(fā)下一樣是不安全的。請看代碼示例:
public class BubbleSort { static volatile int a; public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[20]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(new Runnable() { public void run() { for (int x = 0; x < 10000; x++) { add(); } } }); threads[i].start(); } while (Thread.activeCount() > 2) { Thread.yield(); } System.out.println("a=" + a); } private static void add() { a++; } } 輸出結(jié)果:a=159957
結(jié)果具有不確定性,原因就是a++自增運(yùn)算,不是一個(gè)原子性操作。通過javap -c BubbleSort.class反編譯這段代碼得到add()的字節(jié)碼文件,如下圖所示:
可以看到a++這個(gè)運(yùn)算操作產(chǎn)生了4條字節(jié)碼(return 不是a++產(chǎn)生的),volatile只能保證getstatic時(shí)獲得到a的值是正確的,當(dāng)執(zhí)行其他指令時(shí),很有可能a已經(jīng)是過期數(shù)據(jù)了。事實(shí)上這樣分析是不太嚴(yán)謹(jǐn)?shù)模驗(yàn)樽止?jié)碼最終會(huì)變成cpu指令執(zhí)行,即使只編譯出一條字節(jié)碼指令也不能保證這個(gè)指令就是原子操作。所以如果當(dāng)我們進(jìn)行運(yùn)算的時(shí)候,仍要通過加鎖或者使用concurrent并發(fā)包下的原子類才能保證其原子性。
禁止重排序有一個(gè)非常經(jīng)典的例子,就是DCL單例模式.關(guān)于這篇文章,大神們早已發(fā)過文章對此進(jìn)行闡述了,這里搬運(yùn)一下:
來膜拜下文章署名中的大神們:David Bacon (IBM Research) Joshua Bloch (Javasoft), Jeff Bogda, Cliff Click (Hotspot JVM project), Paul Haahr, Doug Lea, Tom May, Jan-Willem Maessen, Jeremy Manson, John D. Mitchell (jGuru) Kelvin Nilsen, Bill Pugh, Emin Gun Sirer,至少 Joshua Bloch 和 Doug Lea 大家都不陌生吧。
話不多說,上例子:
public class Singleton { private static Singleton instance = null; private Singleton() { this.v = 3; } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
jvm接收到new指令時(shí),簡單分為3步(實(shí)際更多,可參考深入理解虛擬機(jī)),1分配內(nèi)存2實(shí)例化對象3將內(nèi)存地址指向引用。java的內(nèi)存模型并不限制指令的重排序,也就說當(dāng)執(zhí)行步驟從1-》2-》3變成1-》3-》2。當(dāng)線程a訪問走到第2步,未完成實(shí)例化對象前,線程b訪問此對象的返回一個(gè)引用,但若是進(jìn)行其他操作,因?yàn)閷ο蟛]有實(shí)例化,會(huì)造成this逃逸的問題。解決的方法很簡單,就是加上volatile關(guān)鍵字。
volatile小結(jié)
volatile 修飾符適用于以下場景:某個(gè)屬性被多個(gè)線程共享,其中有一個(gè)線程修改了此屬性,其他線程可以立即得到修改后的值。在并發(fā)包的源碼中,它使用得非常多。
volatile 屬性的讀寫操作都是無鎖的,它不能替代 synchronized,因?yàn)樗鼪]有提供原子性和互斥性。因?yàn)闊o鎖,不需要花費(fèi)時(shí)間在獲取鎖和釋放鎖上,所以說它是低成本的。
volatile 只能作用于屬性,我們用 volatile 修飾屬性,這樣 compilers 就不會(huì)對這個(gè)屬性做指令重排序。
volatile 提供了可見性,任何一個(gè)線程對其的修改將立馬對其他線程可見。volatile 屬性不會(huì)被線程緩存,始終從主存中讀取。
volatile 提供了 happens-before 保證,對 volatile 變量 v 的寫入 happens-before 所有其他線程后續(xù)對 v 的讀操作。
volatile 可以使得 long 和 double 的賦值是原子的,前面在說原子性的時(shí)候提到過。
小結(jié)描述該類知識(shí)需要非常嚴(yán)謹(jǐn)?shù)拿枋?,雖然我仔細(xì)檢查了好幾遍,但仍擔(dān)心會(huì)出錯(cuò),一來受限于有限的知識(shí)儲(chǔ)備,二來受限于蹩腳的文字表達(dá)能力。希望讀者可以幫助我指正表達(dá)錯(cuò)誤的地方.
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/68799.html
摘要:物理計(jì)算機(jī)并發(fā)問題在介紹內(nèi)存模型之前,先簡單了解下物理計(jì)算機(jī)中的并發(fā)問題?;诟咚倬彺娴拇鎯?chǔ)交互引入一個(gè)新的問題緩存一致性。寫入作用于主內(nèi)存變量,把操作從工作內(nèi)存中得到的變量值放入主內(nèi)存的變量中。 物理計(jì)算機(jī)并發(fā)問題 在介紹Java內(nèi)存模型之前,先簡單了解下物理計(jì)算機(jī)中的并發(fā)問題。由于處理器的與存儲(chǔ)設(shè)置的運(yùn)算速度有幾個(gè)數(shù)量級的差距,所以現(xiàn)代計(jì)算機(jī)加入一層讀寫速度盡可能接近處理器的高速緩...
摘要:哪吒社區(qū)技能樹打卡打卡貼函數(shù)式接口簡介領(lǐng)域優(yōu)質(zhì)創(chuàng)作者哪吒公眾號(hào)作者架構(gòu)師奮斗者掃描主頁左側(cè)二維碼,加入群聊,一起學(xué)習(xí)一起進(jìn)步歡迎點(diǎn)贊收藏留言前情提要無意間聽到領(lǐng)導(dǎo)們的談話,現(xiàn)在公司的現(xiàn)狀是碼農(nóng)太多,但能獨(dú)立帶隊(duì)的人太少,簡而言之,不缺干 ? 哪吒社區(qū)Java技能樹打卡?【打卡貼 day2...
摘要:基礎(chǔ)問題的的性能及原理之區(qū)別詳解備忘筆記深入理解流水線抽象關(guān)鍵字修飾符知識(shí)點(diǎn)總結(jié)必看篇中的關(guān)鍵字解析回調(diào)機(jī)制解讀抽象類與三大特征時(shí)間和時(shí)間戳的相互轉(zhuǎn)換為什么要使用內(nèi)部類對象鎖和類鎖的區(qū)別,,優(yōu)缺點(diǎn)及比較提高篇八詳解內(nèi)部類單例模式和 Java基礎(chǔ)問題 String的+的性能及原理 java之yield(),sleep(),wait()區(qū)別詳解-備忘筆記 深入理解Java Stream流水...
摘要:基礎(chǔ)問題的的性能及原理之區(qū)別詳解備忘筆記深入理解流水線抽象關(guān)鍵字修飾符知識(shí)點(diǎn)總結(jié)必看篇中的關(guān)鍵字解析回調(diào)機(jī)制解讀抽象類與三大特征時(shí)間和時(shí)間戳的相互轉(zhuǎn)換為什么要使用內(nèi)部類對象鎖和類鎖的區(qū)別,,優(yōu)缺點(diǎn)及比較提高篇八詳解內(nèi)部類單例模式和 Java基礎(chǔ)問題 String的+的性能及原理 java之yield(),sleep(),wait()區(qū)別詳解-備忘筆記 深入理解Java Stream流水...
閱讀 3679·2023-04-25 15:52
閱讀 719·2021-11-19 09:40
閱讀 2961·2021-09-26 09:47
閱讀 1161·2021-09-22 15:17
閱讀 3750·2021-08-13 13:25
閱讀 2483·2019-08-30 15:56
閱讀 3613·2019-08-30 13:56
閱讀 2259·2019-08-30 11:27