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