日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

并发编程之 Java 内存模型 + volatile 关键字 + Happen-Before 规则

發布時間:2025/4/16 java 41 豆豆
生活随笔 收集整理的這篇文章主要介紹了 并发编程之 Java 内存模型 + volatile 关键字 + Happen-Before 规则 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前言

樓主這個標題其實有一種作死的味道,為什么呢,這三個東西其實可以分開為三篇文章來寫,但是,樓主認為這三個東西又都是高度相關的,應當在一個知識點中。在一次學習中去理解這些東西。才能更好的理解 Java 內存模型和 volatile 關鍵字還有 HB 原則。

樓主今天就嘗試著在一篇文章中講述這三個問題,最后總結。

  • 講并發知識前必須復習的硬件知識。
  • Java 內存模型到底是什么玩意?
  • Java 內存模型定義了哪些東西?
  • Java內存模型引出的 Happen-Before 原則是什么?
  • Happen-Before 引出的 volatile 又是什么?
  • 總結這三者。
  • 1. 講并發知識前必須復習的硬件知識。

    首先,因為我們需要了解 Java 虛擬機的并發,而物理硬件的并發和虛擬機的并發很相似,而且虛擬機的并發很多看著奇怪的設計都是因為物理機的設計導致的。

    什么是并發?多個CPU同時執行。但請注意:只有CPU是不行的,CPU 只能計算數據,那么數據從哪里來?

    答案:內存。 數據從內存中來。需要讀取數據,存儲計算結果。有的同學可能會說,不是有寄存器和多級緩存嗎?但是那是靜態隨機訪問內存(Static Random Access Memory),太貴了,SRAM 在設計上使用的晶體管數量較多,價格較高,且不易做成大容量,只能用很小的部分集成的CPU中成為CPU的高速緩存。而正常使用的都是都是動態隨機訪問內存(Dynamic Random Access Memory)。intel 的 CPU 外頻 需要從北橋經過訪問內存,而AMD 的沒有設計北橋,他與 Intel 不同的地方在于,內存是直接與CPU通信而不通過北橋,也就是將內存控制組件集成到CPU中。理論上這樣可以加速CPU和內存的傳輸速度。

    好了,不管哪一家的CPU,都需要從內存中讀取數據,并且自己都有高速緩存或者說寄存器。緩存作什么用呢?由于CPU的速度很快,內存根本跟不上CPU,因此,需要在內存和CPU直接加一層高速緩存讓他們緩沖CPU的數據:將運算需要使用到的數據復制到緩存中,讓運算能夠快速執行,當運算結束后再從緩存同步到內存之中。這樣處理器就無需等待緩慢的內存讀寫了。

    但是這樣引出了另一個問題:緩存一致性(Cache Coherence)。什么意思呢?

    在多處理器中,每個處理器都有自己的高速緩存,而他們又共享同一個主內存(Main Memory),當多個處理器的運算任務都涉及到同一塊主內存區域時,將可能導致各自的緩存數據不一致。如果真的發生這種情況,拿同步到主內存時以誰的緩存數據為準呢?

    在早期的CPU當中,可以通過在總線上加 LOCK# 鎖的形式來解決緩存不一致的問題。因為CPU和其他部件進行通信都是通過總線來進行的,如果對總線加LOCK#鎖的話,也就是說阻塞了其他CPU對其他部件訪問(如內存),從而使得只能有一個CPU能使用這個變量的內存。

    現在的 CPU 為了解決一致性問題,需要各個CPU訪問(讀或者寫)緩存的時候遵循一些協議:MSI,MESI,MOSI,Synapse,Firefly,Dragon Protocol,這些都是緩存一致性協議。

    那么,這個時候需要說一個名詞:內存模型。

    什么是內存模型呢?

    內存模型可以理解為在特定的操作協議下,對特定的內存或高速緩存進行讀寫訪問的過程抽象。不同架構的CPU 有不同的內存模型,而 Java 虛擬機屏蔽了不同CPU內存模型的差異,這就是Java 的內存模型。

    那么 Java 的內存模型的結構是什么樣子的呢?

    好了,關于為什么會有內存模型這件事,我們已經說的差不多了,總體來說就是因為多個CPU的多級緩存訪問同一個內存條可能會導致數據不一致。所以需要一個協議,讓這些處理器在訪問內存的時候遵守這些協議保證數據的一致性。

    還有一個問題。CPU 的流水線執行和亂序執行

    我們假設我們現在有一段代碼:

    int a = 1; int b = 2; int c = a + b;復制代碼

    上面的代碼我們能不能不順序動一下并且結果不變呢?可以,第一行和第二行調換沒有任何問題。

    實際上,CPU 有時候為了優化性能,也會對代碼順序進行調換(在保證結果的前提下),專業術語叫重排序。為什么重排序會優化性能呢?

    這個就有點復雜了,我們慢慢說。

    我們知道,一條指令的執行可以分為很多步驟的,簡單的說,可以分為以下幾步:

  • 取指 IF
  • 譯碼和取寄存器操作數 ID
  • 執行或者有效地址計算 EX
  • 存儲器返回 MEM
  • 寫回 WB
  • 我們的匯編指令也不是一步就可以執行完畢的,在CPU 中實際工作時,他還需要分為多個步驟依次執行,每個步驟涉及到的硬件也可能不同,比如,取指時會用到 PC 寄存器和存儲器,譯碼時會用到指令寄存器組,執行時會使用 ALU,寫回時需要寄存器組。

    也就是說,由于每一個步驟都可能使用不同的硬件完成,因此,CPU 工程師們就發明了流水線技術來執行指令。什么意思呢?

    假如你需要洗車,那么洗車店會執行 “洗車” 這個命令,但是,洗車店會分開操作,比如沖水,打泡沫,洗刷,擦干,打蠟等,這寫動作都可以由不同的員工來做,不需要一個員工依次取執行,其余的員工在那干等著,因此,每個員工都被分配一個任務,執行完就交給下一個員工,就像工廠里的流水線一樣。

    CPU 在執行指令的時候也是這么做的。

    既然是流水線執行,那么流水線肯定不能中斷,否則,一個地方中斷會影響下游所有的組件執行效率,性能損失很大。

    那么怎么辦呢?打個比方,1沖水,2打泡沫,3洗刷,4擦干,5打蠟 本來是按照順序執行的。如果這個時候,水沒有了,那么沖水后面的動作都會收到影響,但是呢,其實我們可以讓沖水先去打水,和打泡沫的換個位置,這樣,我們就先打泡沫,沖水的會在這個時候取接水,等到第一輛車的泡沫打完了,沖水的就回來了,繼續趕回,不影響工作。這個時候順序就變成了:

    1打泡沫 ,2沖水,3洗刷,4擦干,5打蠟.

    但是工作絲毫不受影響。流水線也沒有斷。CPU 中的亂序執行其實也跟這個道理差不多。其最終的目的,還是為了壓榨 CPU 的性能。

    好了,對于今天的文章需要的硬件知識,我們已經復習的差不多了。總結一下,主要是2點:

  • CPU 的多級緩存訪問主存的時候需要配合緩存一致性協議。這個過程可以抽象為內存模型。
  • CPU 為了性能會讓指令流水線執行,并且會在單個 CPU 的執行結構不混亂的情況下亂序執行。
  • 那么,接下來就要好好說說Java 的內存模型了。

    2. Java 內存模型到底是什么玩意?

    回憶下上面的內容,我們說從硬件的層面什么是內存模型?

    內存模型可以理解為在特定的操作協議下,對特定的內存或高速緩存進行讀寫訪問的過程抽象。不同架構的CPU 有不同的內存模型。

    Java 作為跨平臺語言,肯定要屏蔽不同CPU內存模型的差異,構造自己的內存模型,這就是Java 的內存模型。實際上,根源來自硬件的內存模型。

    還是看這個圖片,Java 的內存模型和硬件的內存模型幾乎一樣,每個線程都有自己的工作內存,類似CPU的高速緩存,而 java 的主內存相當于硬件的內存條。

    Java 內存模型也是抽象了線程訪問內存的過程。

    JMM(Java 內存模型)規定了所有的變量都存儲在主內存(這個很重要)中,包括實例字段,靜態字段,和構成數據對象的元素,但不包括局部變量和方法參數,因為后者是線程私有的。不會被共享。自然就沒有競爭問題。

    什么是工作內存呢?每個線程都有自己的工作內存(這個很重要),線程的工作內存保存了該線程使用到的變量和主內存副本拷貝,線程對變量的所有操作(讀寫)都必須在工作內存中進行。而不能直接讀寫主內存中的變量。不同的線程之間也無法訪問對方工作內存中的變量。線程之間變量值的傳遞均需要通過主內存來完成。

    總結一下,Java 內存模型定義了兩個重要的東西,1.主內存,2.工作內存。每個線程的工作內存都是獨立的,線程操作數據只能在工作內存中計算,然后刷入到主存。這是 Java 內存模型定義的線程基本工作方式。

    3. Java 內存模型定義了哪些東西?

    實際上,整個 Java 內存模型圍繞了3個特征建立起來的。這三個特征是整個Java并發的基礎。

    原子性,可見性,有序性。

    原子性(Atomicity)

    什么是原子性,其實這個原子性和事務處理中的原子性定義基本是一樣的。指的是一個操作是不可中斷的,不可分割的。即使在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程干擾。

    我們大致可以認為基本數據類型的訪問讀寫是具備原子性的(但是,如果你在32位虛擬機上計算 long 和 double 就不一樣了),因為 java 虛擬機規范中,對 long 和 double 的操作沒有強制定義要原子性的,但是強烈建議使用原子性的。因此,大部分商用的虛擬機基本都實現了原子性。

    如果用戶需要操作一個更到的范圍保證原子性,那么,Java 內存模型提供了 lock 和 unlock (這是8種內存操操作中的2種)操作來滿足這種需求,但是沒有提供給程序員這兩個操作,提供了更抽象的 monitorenter 和 moniterexit 兩個字節碼指令,也就是 synchronized 關鍵字。因此在 synchronized 塊之間的操作都是原子性的。

    可見性(Visibility)

    可見性是指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改,Java 內存模型是通過在變量修改后將新值同步回主內存,在變量讀取前從主內存刷新變量值,這種依賴主內存作為傳遞媒介的方式來實習那可見性的。無論是普通變量還是 volatile 變量都是如此。他們的區別在于:volatile 的特殊規則保證了新值能立即同步到主內存,以及每次是使用前都能從主內存刷新,因此,可以說 volatile 保證了多線程操作時變量的可見性,而普通變量則不能保證這一點。

    除了 volatile 之外, synchronized 和 final 也能實現可見性。同步塊的可見性是由 對一個變量執行 unlock 操作之前,必須先把此變量同步回主內存種(執行 store, write 操作)

    有序性(Ordering)

    有序性這個問題我們在最上面說硬件的時候說過,CPU 會調整指令順序,同樣的 Java 虛擬機同樣也會調整字節碼順序,但這種調整在單線程里時感知不到的,除非在多線程程序中,這種調整會帶來一些意想不到的錯誤。

    Java 提過了兩個關鍵字來保證多個線程之間操作的有序性,volatile 關鍵字本身就包含了禁止重排序的語義,而 synchronized 則是由 “一個變量同一時刻只允許一條線程對其進行 lock 操作”這個規則獲得的。這條規則決定了同一個鎖的兩個同步塊只能串行的進入。

    好了,介紹完了 JMM 的三種基本特征。不知道大家有沒有發現,volatile 保證了可見性和有序性,synchronized 則3個特性都保證了,堪稱萬能。而且 synchronized 使用方便。但是,仍然要警惕他對性能的影響。

    4. Java內存模型引出的 Happen-Before 原則是什么?

    說到有序性,注意,我們說有序性可以通過 volatile 和 synchronized 來實現,但是我們不可能所有的代碼都靠這兩個關鍵字。實際上,Java 語言已對重排序或者說有序性做了規定,這些規定在虛擬機優化的時候是不能違背的。

  • 程序次序原則:一個線程內,按照程序代碼順序,書寫在前面的操作先發生于書寫在后面的操作。
  • volatile 規則:volatile 變量的寫,先發生于讀,這保證了 volatile 變量的可見性。
  • 鎖規則:解鎖(unlock) 必然發生在隨后的加鎖(lock)前。
  • 傳遞性:A先于B,B先于C,那么A必然先于C。
  • 線程的 start 方法先于他的每一個動作。
  • 線程的所有操作先于線程的終結。
  • 線程的中斷(interrupt())先于被中斷的代碼。
  • 對象的構造函數,結束先于 finalize 方法。
  • 5. Happen-Before 引出的 volatile 又是什么?

    我們在前面,說了很多的 volatile 關鍵字,可見這個關鍵字非常的重要,但似乎他的使用頻率比 synchronized 少多了,我們知道了這個關鍵字可以做什么呢?

    volatile 可以實現線程的可見性,還可以實現線程的有序性。但是不能實現原子性。

    我們還是直接寫一段代碼吧!

    package cn.think.in.java.two;/*** volatile 不能保證原子性,只能遵守 hp 原則 保證單線程的有序性和可見性。*/ public class MultitudeTest {static volatile int i = 0;static class PlusTask implements Runnable {@Overridepublic void run() {for (int j = 0; j < 10000; j++) { // plusI();i++;}}}public static void main(String[] args) throws InterruptedException {Thread[] threads = new Thread[10];for (int j = 0; j < 10; j++) {threads[j] = new Thread(new PlusTask());threads[j].start();}for (int j = 0; j < 10; j++) {threads[j].join();}System.out.println(i);}// static synchronized void plusI() { // i++; // }}復制代碼

    我們啟動了10個線程分別對一個 int 變量進行 ++ 操作,注意,++ 符號不是原子的。然后,主線程等待在這10個線程上,執行結束后打印 int 值。你會發現,無論怎么運行都到不了10000,因為他不是原子的。怎么理解呢?

    i++ 等于 i = i + 1;

    虛擬機首先讀取 i 的值,然后在 i 的基礎上加1,請注意,volatile 保證了線程讀取的值是最新的,當線程讀取 i 的時候,該值確實是最新的,但是有10個線程都去讀了,他們讀到的都是最新的,并且同時加1,這些操作不違法 volatile 的定義。最終出現錯誤,可以說是我們使用不當。

    樓主也在測試代碼中加入了一個同步方法,同步方法能夠保證原子性。當for循環中執行的不是i++,而是 plusI 方法,那么結果就會準確了。

    那么,什么時候用 volatile 呢?

    運算結果并不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。 我們程序的情況就是,運算結果依賴 i 當前的值,如果改為 原子操作: i = j,那么結果就會是正確的 9999.

    比如下面這個程序就是使用 volatile 的范例:

    package cn.think.in.java.two;/*** java 內存模型:* 單線程下會重排序。* 下面這段程序再 -server 模式下會優化代碼(重排序),導致永遠死循環。*/ public class JMMDemo {// static boolean ready;static volatile boolean ready;static int num;static class ReaderThread extends Thread {public void run() {while (!ready) {}System.out.println(num);}}public static void main(String[] args) throws InterruptedException {new ReaderThread().start();Thread.sleep(1000);num = 32;ready = true;Thread.sleep(1000);Thread.yield();}} 復制代碼

    這段程序很有意思,我們使用 volatile 變量來控制流程,最終的正確結果是32,但是請注意,如果你沒有使用 volatile 關鍵字,并且虛擬機啟動的時候加入了 -server參數,這段程序將永遠不會結束,因為他會被 JIT 優化并且另一個線程永遠無法看到變量的修改(JIT 會忽略他認為無效的代碼)。當然,當你修改為 volatile 就沒有任何問題了。

    通過上面的代碼,我們知道了,volatile 確實不能保證原子性,但是能保證有序性和可見性。那么是怎么實現的呢?

    怎么保證有序性呢?實際上,在操作 volatile 關鍵字變量前后的匯編代碼中,會有一個 lock 前綴,根據 intel IA32 手冊,lock 的作用是 使得 本 CPU 的Cache 寫入了內存,該寫入動作也會引起別的CPU或者別的內核無效化其Cache,別的CPU需要重新獲取Cache。這樣就實現了可見性。可見底層還是使用的 CPU 的指令。

    如何實現有序性呢?同樣是lock 指令,這個指令還相當于一個內存屏障(大多數現代計算機為了提高性能而采取亂序執行,這使得內存屏障成為必須。語義上,內存屏障之前的所有寫操作都要寫入內存;內存屏障之后的讀操作都可以獲得同步屏障之前的寫操作的結果。因此,對于敏感的程序塊,寫操作之后、讀操作之前可以插入內存屏障),指的是,重排序時不能把后面的指令重排序到內存屏障之前的位置。只有一個CPU訪問內存時,并不需要內存屏障;但如果有兩個或者更多CPU訪問同一塊內存,且其中有一個在觀測另一個,就需要內存屏障來保證了。

    因此請不要隨意使用 volatile 變量,這會導致 JIT 無法優化代碼,并且會插入很多的內存屏障指令,降低性能。

    6. 總結

    首先 JMM 是抽象化了硬件的內存模型(使用了多級緩存導致出現緩存一致性協議),屏蔽了各個 CPU 和操作系統的差異。

    Java 內存模型指的是:在特定的協議下對內存的訪問過程。也就是線程的工作內存和主存直接的操作順序。

    JMM 主要圍繞著原子性,可見性,有序性來設置規范。

    synchronized 可以實現這3個功能,而 volatile 只能實現可見性和有序性。final 也能是實現可見性。

    Happen-Before 原則規定了哪些是虛擬機不能重排序的,其中包括了鎖的規定,volatile 變量的讀與寫規定。

    而 volatile 我們也說了,不能保證原子性,所以使用的時候需要注意。volatile 底層的實現還是 CPU 的 lock 指令,通過刷新其余的CPU 的Cache 保證可見性,通過內存柵欄保證了有序性。

    總的來說,這3個概念可以說息息相關。他們之間互相依賴。所以樓主放在了一篇來寫,但這可能會導致有所疏漏,但不妨礙我們了解整個的概念。可以說,JMM 是所有并發編程的基礎,如果不了解 JMM,根本不可能高效并發。

    當然,我們這篇文章還是不夠底層,并沒有剖析 JVM 內部是怎么實現的,今天已經很晚了,有機會,我們一起進入 JVM 源碼查看他們的底層實現。

    good luck!!!!

    總結

    以上是生活随笔為你收集整理的并发编程之 Java 内存模型 + volatile 关键字 + Happen-Before 规则的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。