并發(fā)編程之 Java 內(nèi)存模型 + volatile 關(guān)鍵字 + Happen-Before 規(guī)則

JMM---- Java 并發(fā)的基礎(chǔ)知識(shí)

前言

樓主這個(gè)標(biāo)題其實(shí)有一種作死的味道,為什么呢,這三個(gè)東西其實(shí)可以分開(kāi)為三篇文章來(lái)寫(xiě),但是,樓主認(rèn)為這三個(gè)東西又都是高度相關(guān)的,應(yīng)當(dāng)在一個(gè)知識(shí)點(diǎn)中。在一次學(xué)習(xí)中去理解這些東西。才能更好的理解 Java 內(nèi)存模型和 volatile 關(guān)鍵字還有 HB 原則。

樓主今天就嘗試著在一篇文章中講述這三個(gè)問(wèn)題,最后總結(jié)。

  1. 講并發(fā)知識(shí)前必須復(fù)習(xí)的硬件知識(shí)。
  2. Java 內(nèi)存模型到底是什么玩意?
  3. Java 內(nèi)存模型定義了哪些東西?
  4. Java內(nèi)存模型引出的 Happen-Before 原則是什么?
  5. Happen-Before 引出的 volatile 又是什么?
  6. 總結(jié)這三者。

1. 講并發(fā)知識(shí)前必須復(fù)習(xí)的硬件知識(shí)。

首先,因?yàn)槲覀冃枰私?Java 虛擬機(jī)的并發(fā),而物理硬件的并發(fā)和虛擬機(jī)的并發(fā)很相似,而且虛擬機(jī)的并發(fā)很多看著奇怪的設(shè)計(jì)都是因?yàn)槲锢頇C(jī)的設(shè)計(jì)導(dǎo)致的。

什么是并發(fā)?多個(gè)CPU同時(shí)執(zhí)行。但請(qǐng)注意:只有CPU是不行的,CPU 只能計(jì)算數(shù)據(jù),那么數(shù)據(jù)從哪里來(lái)?

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

好了,不管哪一家的CPU,都需要從內(nèi)存中讀取數(shù)據(jù),并且自己都有高速緩存或者說(shuō)寄存器。緩存作什么用呢?由于CPU的速度很快,內(nèi)存根本跟不上CPU,因此,需要在內(nèi)存和CPU直接加一層高速緩存讓他們緩沖CPU的數(shù)據(jù):將運(yùn)算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能夠快速執(zhí)行,當(dāng)運(yùn)算結(jié)束后再?gòu)木彺嫱降絻?nèi)存之中。這樣處理器就無(wú)需等待緩慢的內(nèi)存讀寫(xiě)了。

CPU 和緩存

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

在多處理器中,每個(gè)處理器都有自己的高速緩存,而他們又共享同一個(gè)主內(nèi)存(Main Memory),當(dāng)多個(gè)處理器的運(yùn)算任務(wù)都涉及到同一塊主內(nèi)存區(qū)域時(shí),將可能導(dǎo)致各自的緩存數(shù)據(jù)不一致。如果真的發(fā)生這種情況,拿同步到主內(nèi)存時(shí)以誰(shuí)的緩存數(shù)據(jù)為準(zhǔn)呢?

在早期的CPU當(dāng)中,可以通過(guò)在總線上加 LOCK# 鎖的形式來(lái)解決緩存不一致的問(wèn)題。因?yàn)镃PU和其他部件進(jìn)行通信都是通過(guò)總線來(lái)進(jìn)行的,如果對(duì)總線加LOCK#鎖的話,也就是說(shuō)阻塞了其他CPU對(duì)其他部件訪問(wèn)(如內(nèi)存),從而使得只能有一個(gè)CPU能使用這個(gè)變量的內(nèi)存。

現(xiàn)在的 CPU 為了解決一致性問(wèn)題,需要各個(gè)CPU訪問(wèn)(讀或者寫(xiě))緩存的時(shí)候遵循一些協(xié)議:MSI,MESI,MOSI,Synapse,F(xiàn)irefly,Dragon Protocol,這些都是緩存一致性協(xié)議。

那么,這個(gè)時(shí)候需要說(shuō)一個(gè)名詞:內(nèi)存模型。

什么是內(nèi)存模型呢?

內(nèi)存模型可以理解為在特定的操作協(xié)議下,對(duì)特定的內(nèi)存或高速緩存進(jìn)行讀寫(xiě)訪問(wèn)的過(guò)程抽象。不同架構(gòu)的CPU 有不同的內(nèi)存模型,而 Java 虛擬機(jī)屏蔽了不同CPU內(nèi)存模型的差異,這就是Java 的內(nèi)存模型。

那么 Java 的內(nèi)存模型的結(jié)構(gòu)是什么樣子的呢?

Java 內(nèi)存模型(Java Memory Model)

好了,關(guān)于為什么會(huì)有內(nèi)存模型這件事,我們已經(jīng)說(shuō)的差不多了,總體來(lái)說(shuō)就是因?yàn)槎鄠€(gè)CPU的多級(jí)緩存訪問(wèn)同一個(gè)內(nèi)存條可能會(huì)導(dǎo)致數(shù)據(jù)不一致。所以需要一個(gè)協(xié)議,讓這些處理器在訪問(wèn)內(nèi)存的時(shí)候遵守這些協(xié)議保證數(shù)據(jù)的一致性。

還有一個(gè)問(wèn)題。CPU 的流水線執(zhí)行和亂序執(zhí)行

我們假設(shè)我們現(xiàn)在有一段代碼:


int a = 1;
int b = 2;
int c = a + b;

上面的代碼我們能不能不順序動(dòng)一下并且結(jié)果不變呢?可以,第一行和第二行調(diào)換沒(méi)有任何問(wèn)題。

實(shí)際上,CPU 有時(shí)候?yàn)榱藘?yōu)化性能,也會(huì)對(duì)代碼順序進(jìn)行調(diào)換(在保證結(jié)果的前提下),專(zhuān)業(yè)術(shù)語(yǔ)叫重排序。為什么重排序會(huì)優(yōu)化性能呢?

這個(gè)就有點(diǎn)復(fù)雜了,我們慢慢說(shuō)。

我們知道,一條指令的執(zhí)行可以分為很多步驟的,簡(jiǎn)單的說(shuō),可以分為以下幾步:

  1. 取指 IF
  2. 譯碼和取寄存器操作數(shù) ID
  3. 執(zhí)行或者有效地址計(jì)算 EX
  4. 存儲(chǔ)器返回 MEM
  5. 寫(xiě)回 WB

我們的匯編指令也不是一步就可以執(zhí)行完畢的,在CPU 中實(shí)際工作時(shí),他還需要分為多個(gè)步驟依次執(zhí)行,每個(gè)步驟涉及到的硬件也可能不同,比如,取指時(shí)會(huì)用到 PC 寄存器和存儲(chǔ)器,譯碼時(shí)會(huì)用到指令寄存器組,執(zhí)行時(shí)會(huì)使用 ALU,寫(xiě)回時(shí)需要寄存器組。

也就是說(shuō),由于每一個(gè)步驟都可能使用不同的硬件完成,因此,CPU 工程師們就發(fā)明了流水線技術(shù)來(lái)執(zhí)行指令。什么意思呢?

假如你需要洗車(chē),那么洗車(chē)店會(huì)執(zhí)行 “洗車(chē)” 這個(gè)命令,但是,洗車(chē)店會(huì)分開(kāi)操作,比如沖水,打泡沫,洗刷,擦干,打蠟等,這寫(xiě)動(dòng)作都可以由不同的員工來(lái)做,不需要一個(gè)員工依次取執(zhí)行,其余的員工在那干等著,因此,每個(gè)員工都被分配一個(gè)任務(wù),執(zhí)行完就交給下一個(gè)員工,就像工廠里的流水線一樣。

CPU 在執(zhí)行指令的時(shí)候也是這么做的。

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

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

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

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

好了,對(duì)于今天的文章需要的硬件知識(shí),我們已經(jīng)復(fù)習(xí)的差不多了??偨Y(jié)一下,主要是2點(diǎn):

  1. CPU 的多級(jí)緩存訪問(wèn)主存的時(shí)候需要配合緩存一致性協(xié)議。這個(gè)過(guò)程可以抽象為內(nèi)存模型。
  2. CPU 為了性能會(huì)讓指令流水線執(zhí)行,并且會(huì)在單個(gè) CPU 的執(zhí)行結(jié)構(gòu)不混亂的情況下亂序執(zhí)行。

那么,接下來(lái)就要好好說(shuō)說(shuō)Java 的內(nèi)存模型了。

2. Java 內(nèi)存模型到底是什么玩意?

回憶下上面的內(nèi)容,我們說(shuō)從硬件的層面什么是內(nèi)存模型?

內(nèi)存模型可以理解為在特定的操作協(xié)議下,對(duì)特定的內(nèi)存或高速緩存進(jìn)行讀寫(xiě)訪問(wèn)的過(guò)程抽象。不同架構(gòu)的CPU 有不同的內(nèi)存模型。

Java 作為跨平臺(tái)語(yǔ)言,肯定要屏蔽不同CPU內(nèi)存模型的差異,構(gòu)造自己的內(nèi)存模型,這就是Java 的內(nèi)存模型。實(shí)際上,根源來(lái)自硬件的內(nèi)存模型。

Java 內(nèi)存模型(Java Memory Model)

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

Java 內(nèi)存模型也是抽象了線程訪問(wèn)內(nèi)存的過(guò)程。

JMM(Java 內(nèi)存模型)規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存(這個(gè)很重要)中,包括實(shí)例字段,靜態(tài)字段,和構(gòu)成數(shù)據(jù)對(duì)象的元素,但不包括局部變量和方法參數(shù),因?yàn)楹笳呤蔷€程私有的。不會(huì)被共享。自然就沒(méi)有競(jìng)爭(zhēng)問(wèn)題。

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

總結(jié)一下,Java 內(nèi)存模型定義了兩個(gè)重要的東西,1.主內(nèi)存,2.工作內(nèi)存。每個(gè)線程的工作內(nèi)存都是獨(dú)立的,線程操作數(shù)據(jù)只能在工作內(nèi)存中計(jì)算,然后刷入到主存。這是 Java 內(nèi)存模型定義的線程基本工作方式。

3. Java 內(nèi)存模型定義了哪些東西?

實(shí)際上,整個(gè) Java 內(nèi)存模型圍繞了3個(gè)特征建立起來(lái)的。這三個(gè)特征是整個(gè)Java并發(fā)的基礎(chǔ)。

原子性,可見(jiàn)性,有序性。

原子性(Atomicity)

什么是原子性,其實(shí)這個(gè)原子性和事務(wù)處理中的原子性定義基本是一樣的。指的是一個(gè)操作是不可中斷的,不可分割的。即使在多個(gè)線程一起執(zhí)行的時(shí)候,一個(gè)操作一旦開(kāi)始,就不會(huì)被其他線程干擾。

我們大致可以認(rèn)為基本數(shù)據(jù)類(lèi)型的訪問(wèn)讀寫(xiě)是具備原子性的(但是,如果你在32位虛擬機(jī)上計(jì)算 long 和 double 就不一樣了),因?yàn)?java 虛擬機(jī)規(guī)范中,對(duì) long 和 double 的操作沒(méi)有強(qiáng)制定義要原子性的,但是強(qiáng)烈建議使用原子性的。因此,大部分商用的虛擬機(jī)基本都實(shí)現(xiàn)了原子性。

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

可見(jiàn)性(Visibility)

可見(jiàn)性是指當(dāng)一個(gè)線程修改了共享變量的值,其他線程能夠立即得知這個(gè)修改,Java 內(nèi)存模型是通過(guò)在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值,這種依賴(lài)主內(nèi)存作為傳遞媒介的方式來(lái)實(shí)習(xí)那可見(jiàn)性的。無(wú)論是普通變量還是 volatile 變量都是如此。他們的區(qū)別在于:volatile 的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每次是使用前都能從主內(nèi)存刷新,因此,可以說(shuō) volatile 保證了多線程操作時(shí)變量的可見(jiàn)性,而普通變量則不能保證這一點(diǎn)。

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

有序性(Ordering)

有序性這個(gè)問(wèn)題我們?cè)谧钌厦嬲f(shuō)硬件的時(shí)候說(shuō)過(guò),CPU 會(huì)調(diào)整指令順序,同樣的 Java 虛擬機(jī)同樣也會(huì)調(diào)整字節(jié)碼順序,但這種調(diào)整在單線程里時(shí)感知不到的,除非在多線程程序中,這種調(diào)整會(huì)帶來(lái)一些意想不到的錯(cuò)誤。

Java 提過(guò)了兩個(gè)關(guān)鍵字來(lái)保證多個(gè)線程之間操作的有序性,volatile 關(guān)鍵字本身就包含了禁止重排序的語(yǔ)義,而 synchronized 則是由 “一個(gè)變量同一時(shí)刻只允許一條線程對(duì)其進(jìn)行 lock 操作”這個(gè)規(guī)則獲得的。這條規(guī)則決定了同一個(gè)鎖的兩個(gè)同步塊只能串行的進(jìn)入。

好了,介紹完了 JMM 的三種基本特征。不知道大家有沒(méi)有發(fā)現(xiàn),volatile 保證了可見(jiàn)性和有序性,synchronized 則3個(gè)特性都保證了,堪稱(chēng)萬(wàn)能。而且 synchronized 使用方便。但是,仍然要警惕他對(duì)性能的影響。

4. Java內(nèi)存模型引出的 Happen-Before 原則是什么?

說(shuō)到有序性,注意,我們說(shuō)有序性可以通過(guò) volatile 和 synchronized 來(lái)實(shí)現(xiàn),但是我們不可能所有的代碼都靠這兩個(gè)關(guān)鍵字。實(shí)際上,Java 語(yǔ)言已對(duì)重排序或者說(shuō)有序性做了規(guī)定,這些規(guī)定在虛擬機(jī)優(yōu)化的時(shí)候是不能違背的。

  1. 程序次序原則:一個(gè)線程內(nèi),按照程序代碼順序,書(shū)寫(xiě)在前面的操作先發(fā)生于書(shū)寫(xiě)在后面的操作。
  2. volatile 規(guī)則:volatile 變量的寫(xiě),先發(fā)生于讀,這保證了 volatile 變量的可見(jiàn)性。
  3. 鎖規(guī)則:解鎖(unlock) 必然發(fā)生在隨后的加鎖(lock)前。
  4. 傳遞性:A先于B,B先于C,那么A必然先于C。
  5. 線程的 start 方法先于他的每一個(gè)動(dòng)作。
  6. 線程的所有操作先于線程的終結(jié)。
  7. 線程的中斷(interrupt())先于被中斷的代碼。
  8. 對(duì)象的構(gòu)造函數(shù),結(jié)束先于 finalize 方法。

5. Happen-Before 引出的 volatile 又是什么?

我們?cè)谇懊?,說(shuō)了很多的 volatile 關(guān)鍵字,可見(jiàn)這個(gè)關(guān)鍵字非常的重要,但似乎他的使用頻率比 synchronized
少多了,我們知道了這個(gè)關(guān)鍵字可以做什么呢?

volatile 可以實(shí)現(xiàn)線程的可見(jiàn)性,還可以實(shí)現(xiàn)線程的有序性。但是不能實(shí)現(xiàn)原子性。

我們還是直接寫(xiě)一段代碼吧!

package cn.think.in.java.two;

/**
 * volatile 不能保證原子性,只能遵守 hp 原則 保證單線程的有序性和可見(jiàn)性。
 */
public class MultitudeTest {

  static volatile int i = 0;

  static class PlusTask implements Runnable {

    @Override
    public 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++;
//  }

}

我們啟動(dòng)了10個(gè)線程分別對(duì)一個(gè) int 變量進(jìn)行 ++ 操作,注意,++ 符號(hào)不是原子的。然后,主線程等待在這10個(gè)線程上,執(zhí)行結(jié)束后打印 int 值。你會(huì)發(fā)現(xiàn),無(wú)論怎么運(yùn)行都到不了10000,因?yàn)樗皇窃拥摹T趺蠢斫饽兀?/p>

i++ 等于 i = i + 1;

虛擬機(jī)首先讀取 i 的值,然后在 i 的基礎(chǔ)上加1,請(qǐng)注意,volatile 保證了線程讀取的值是最新的,當(dāng)線程讀取 i 的時(shí)候,該值確實(shí)是最新的,但是有10個(gè)線程都去讀了,他們讀到的都是最新的,并且同時(shí)加1,這些操作不違法 volatile 的定義。最終出現(xiàn)錯(cuò)誤,可以說(shuō)是我們使用不當(dāng)。

樓主也在測(cè)試代碼中加入了一個(gè)同步方法,同步方法能夠保證原子性。當(dāng)for循環(huán)中執(zhí)行的不是i++,而是 plusI 方法,那么結(jié)果就會(huì)準(zhǔn)確了。

那么,什么時(shí)候用 volatile 呢?

運(yùn)算結(jié)果并不依賴(lài)變量的當(dāng)前值,或者能夠確保只有單一的線程修改變量的值。
我們程序的情況就是,運(yùn)算結(jié)果依賴(lài) i 當(dāng)前的值,如果改為 原子操作: i = j,那么結(jié)果就會(huì)是正確的 9999.

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

package cn.think.in.java.two;

/**
 * java 內(nèi)存模型:
 * 單線程下會(huì)重排序。
 * 下面這段程序再 -server 模式下會(huì)優(yōu)化代碼(重排序),導(dǎo)致永遠(yuǎn)死循環(huán)。
 */
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 變量來(lái)控制流程,最終的正確結(jié)果是32,但是請(qǐng)注意,如果你沒(méi)有使用 volatile 關(guān)鍵字,并且虛擬機(jī)啟動(dòng)的時(shí)候加入了 -server參數(shù),這段程序?qū)⒂肋h(yuǎn)不會(huì)結(jié)束,因?yàn)樗麜?huì)被 JIT 優(yōu)化并且另一個(gè)線程永遠(yuǎn)無(wú)法看到變量的修改(JIT 會(huì)忽略他認(rèn)為無(wú)效的代碼)。當(dāng)然,當(dāng)你修改為 volatile 就沒(méi)有任何問(wèn)題了。

通過(guò)上面的代碼,我們知道了,volatile 確實(shí)不能保證原子性,但是能保證有序性和可見(jiàn)性。那么是怎么實(shí)現(xiàn)的呢?

怎么保證有序性呢?實(shí)際上,在操作 volatile 關(guān)鍵字變量前后的匯編代碼中,會(huì)有一個(gè) lock 前綴,根據(jù) intel IA32 手冊(cè),lock 的作用是 使得 本 CPU 的Cache 寫(xiě)入了內(nèi)存,該寫(xiě)入動(dòng)作也會(huì)引起別的CPU或者別的內(nèi)核無(wú)效化其Cache,別的CPU需要重新獲取Cache。這樣就實(shí)現(xiàn)了可見(jiàn)性??梢?jiàn)底層還是使用的 CPU 的指令。

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

因此請(qǐng)不要隨意使用 volatile 變量,這會(huì)導(dǎo)致 JIT 無(wú)法優(yōu)化代碼,并且會(huì)插入很多的內(nèi)存屏障指令,降低性能。

6. 總結(jié)

首先 JMM 是抽象化了硬件的內(nèi)存模型(使用了多級(jí)緩存導(dǎo)致出現(xiàn)緩存一致性協(xié)議),屏蔽了各個(gè) CPU 和操作系統(tǒng)的差異。

Java 內(nèi)存模型指的是:在特定的協(xié)議下對(duì)內(nèi)存的訪問(wèn)過(guò)程。也就是線程的工作內(nèi)存和主存直接的操作順序。

JMM 主要圍繞著原子性,可見(jiàn)性,有序性來(lái)設(shè)置規(guī)范。

synchronized 可以實(shí)現(xiàn)這3個(gè)功能,而 volatile 只能實(shí)現(xiàn)可見(jiàn)性和有序性。final 也能是實(shí)現(xiàn)可見(jiàn)性。

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

而 volatile 我們也說(shuō)了,不能保證原子性,所以使用的時(shí)候需要注意。volatile 底層的實(shí)現(xiàn)還是 CPU 的 lock 指令,通過(guò)刷新其余的CPU 的Cache 保證可見(jiàn)性,通過(guò)內(nèi)存柵欄保證了有序性。

總的來(lái)說(shuō),這3個(gè)概念可以說(shuō)息息相關(guān)。他們之間互相依賴(lài)。所以樓主放在了一篇來(lái)寫(xiě),但這可能會(huì)導(dǎo)致有所疏漏,但不妨礙我們了解整個(gè)的概念??梢哉f(shuō),JMM 是所有并發(fā)編程的基礎(chǔ),如果不了解 JMM,根本不可能高效并發(fā)。

當(dāng)然,我們這篇文章還是不夠底層,并沒(méi)有剖析 JVM 內(nèi)部是怎么實(shí)現(xiàn)的,今天已經(jīng)很晚了,有機(jī)會(huì),我們一起進(jìn)入 JVM 源碼查看他們的底層實(shí)現(xiàn)。

good luck!?。?!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容