作者:小牛呼嚕嚕 | https://xiaoniuhululu.com
計算機內功、JAVA底層、面試相關資料等更多精彩文章在公眾號「小牛呼嚕嚕 」
在上一篇文章https://mp.weixin.qq.com/s/KecubYROQztHvwPicJb9wQ中,我們瞭解了計算機由於各個硬體的讀取速度之間的巨大差距,和充分利用CPU的效能的手段方法,及其所帶來的一系列問題:
有序性問題
。可見性問題
原子性問題
。Java 是最早嘗試提供記憶體模型的程式語言。由於Java 語言是跨平臺的,另外各個作業系統總存在一些差異,Java在物理機器的基礎上抽象出一個"記憶體模型(JMM)"
JMM 可以看作是 Java 定義的並行程式設計相關的一組規範,除了抽象了執行緒和主記憶體之間的關係之外,其還規定了從 Java 原始碼到 CPU 可執行指令的這個轉化過程要遵守哪些和並行相關的原則和規範,這樣就可以遮蔽各個作業系統的差異,簡化多執行緒程式設計。
這是一個非常容易讓人混淆的問題,Java 記憶體區域和記憶體模型完全是不一樣的東西,
Java 記憶體區域
, 也叫記憶體區域
、JVM記憶體模型
,和 Java 虛擬機器器(JVM)的執行時區域相關,是指 JVM執行時將資料分割區域儲存,強調對記憶體空間的劃分。Java記憶體模型
,也叫記憶體模型(JMM)
,是Java 定義的並行程式設計相關的一組規範,除了抽象了執行緒和主記憶體之間的關係之外,其還規定了從 Java 原始碼到 CPU 可執行指令的這個轉化過程要遵守哪些和並行相關的原則和規範,遮蔽各個作業系統的差異。通俗點說:JMM規範了程式中變數的存取規則,保證了操作的原子性、可見性、有序性,我們下文慢慢道來。我們知道JVM 執行時記憶體區域是分割區域的,分為棧、堆等,其實這些都是 JVM 定義的邏輯概念。但在傳統的硬體記憶體架構中是沒有棧和堆這種概念。
其中:
虛擬機器器棧(JVM Stacks)
和 本地方法棧(Native Method Stack)
JNI(Java Native Interface)
來存取虛擬機器器執行時的資料區,甚至可以呼叫暫存器,具有和 JVM 相同的能力和許可權。 JNI 類本地方法最著名的應該是 System.currentTimeMillis()
虛擬機器器堆是Java虛擬機器器中記憶體最大的一塊,是被所有執行緒共用的,在虛擬機器器啟動時候建立,Java堆唯一的目的就是存放物件範例,幾乎所有的物件範例都在這裡分配記憶體,隨著JIT編譯器的發展和逃逸分析技術的逐漸成熟,棧上分配、標量替換優化的技術將會導致一些微妙的變化,所有的物件都分配在堆上漸漸變得不那麼「絕對」了。
Java中棧和堆
既存在於計算機的快取記憶體中,又存在於主記憶體
中,所以兩者並沒有很直接的關係。
Java 記憶體模型(JMM) 抽象了執行緒和主記憶體之間的關係,就比如說執行緒之間的共用變數必須儲存在主記憶體中。
在 JDK1.2 之前,Java 的記憶體模型實現總是從 主記憶體 (即共用記憶體)讀取變數,是不需要進行特別的注意的。而在當前的 Java 記憶體模型下,執行緒可以把變數儲存 本地記憶體 (比如機器的暫存器)中,而不是直接在主記憶體中進行讀寫。這就可能造成一個執行緒在主記憶體中修改了一個變數的值,而另外一個執行緒還繼續使用它在暫存器中的變數值的拷貝,造成資料的不一致。
JMM
抽象出來的一個概念,儲存了主記憶體中的共用變數副本。Java 記憶體模型其實是一種規範,定義了很多東西:
這裡所講的主記憶體、工作記憶體與 Java 記憶體區域中的 Java 堆、棧、方法區等並不是同一個層次的記憶體劃分,這兩者基本上是沒有關係的,如果兩者一定要勉強對應起來,那從變數、主記憶體、工作記憶體的定義來看,主記憶體主要對應於Java堆中的物件範例資料部分,而工作記憶體則對應於虛擬機器器棧中的部分割區域。
執行緒間的通訊一般有兩種方式進行,一是通過訊息傳遞
,二是共用記憶體
。Java 執行緒間的通訊採用的是共用記憶體方式,JMM 為共用變數提供了執行緒間的保障。如果兩個執行緒都對一個共用變數進行操作,共用變數初始值為 1,每個執行緒都變數進行加 1,預期共用變數的值為 3。在 JMM 規範下會有一系列的操作。我們直接來看下圖:
在多執行緒的情況下,對主記憶體中的共用變數進行操作可能發生執行緒安全問題,比如:執行緒 1 和執行緒 2 同時對同一個共用變數進行操作,執行+1
操作,執行緒 1 、執行緒2 讀取的共用變數是否是彼此修改前還是修改後的值呢,這個是無法確定的,這種情況和CPU的快取記憶體與記憶體之間的問題非常相似
如何實現主記憶體與工作記憶體的變數同步,為了更好的控制主記憶體和本地記憶體的互動,Java 記憶體模型定義了八種操作來實現:
工作記憶體即本地記憶體
。原子性:即一個或者多個操作作為一個整體,要麼全部執行,要麼都不執行,並且操作在執行過程中不會被執行緒排程機制打斷;而且這種操作一旦開始,就一直執行到結束,中間不會有任何上下文切換(context switch)
比如:
int i = 0; //語句1,原子性
i++; //語句2,非原子性
語句1大家一幕瞭然,語句2卻許多人容易犯迷糊,i++
其實可以分為3步:
執行上述3個步驟的時候是可以進行執行緒切換的,或者說是可以被另其他執行緒的 這3 步打斷的,因此語句2
不是一個原子性操作
在 Java 中,可以藉助synchronized
、各種 Lock
以及各種原子類實現原子性。
synchronized
和各種Lock
是通過保證任一時刻只有一個執行緒存取該程式碼塊,因此可以保證其原子性。各種原子類是利用CAS (compare and swap)
操作(可能也會用到 volatile
或者final
關鍵字)來保證原子操作。
可見性是指當多個執行緒存取同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看到修改的值。
我們來看一個例子:
public class VisibilityTest {
private boolean flag = true;
public void change() {
flag = false;
System.out.println(Thread.currentThread().getName() + ",已修改flag=false");
}
public void load() {
System.out.println(Thread.currentThread().getName() + ",開始執行.....");
int i = 0;
while (flag) {
i++;
}
System.out.println(Thread.currentThread().getName() + ",結束迴圈");
}
public static void main(String[] args) throws InterruptedException {
VisibilityTest test = new VisibilityTest();
// 執行緒threadA模擬資料載入場景
Thread threadA = new Thread(() -> test.load(), "threadA");
threadA.start();
// 讓threadA執行一會兒
Thread.sleep(1000);
// 執行緒threadB 修改 共用變數flag
Thread threadB = new Thread(() -> test.change(), "threadB");
threadB.start();
}
}
threadA 負責迴圈,threadB負責修改 共用變數flag
,如果flag=false時,threadA 會結束迴圈,但是上面的例子會死迴圈。原因是threadA無法立即讀取到共用變數flag修改後的值。我們只需 private volatile boolean flag = true;
加上volatile
關鍵字threadA就可以立即退出迴圈了。
Java中的volatile關鍵字
提供了一個功能,那就是被其修飾的變數在被修改後可以立即同步到主記憶體,被其修飾的變數在每次是用之前都從主記憶體重新整理。
因此,可以使用volatile
來保證多執行緒操作時變數的可見性。除了volatile
,Java中的synchronized
和final
兩個關鍵字 以及各種 Lock也可以實現可見性。
有序性:即程式執行的順序按照程式碼的先後順序執行。
int i = 0;
int j = 0;
i = 10; //語句1
j = 1; //語句2
但由於指令重排序問題,程式碼的執行順序未必就是編寫程式碼時候的順序。語句可能的執行順序如下:
指令重排對於非原子性的操作,在不影響最終結果的情況下,其拆分成的原子操作可能會被重新排列執行順序。指令重排不會影響單執行緒的執行結果,但是會影響多執行緒並行執行的結果正確性。
在Java 中,可以通過volatile關鍵字
來禁止指令進行重排序優化,詳情可見:https://mp.weixin.qq.com/s/TyiCfVMeeDwa-2hd9N9XJQ。也可以使用synchronized關鍵字
保證同一時刻只允許一條執行緒存取程式塊。
參考資料:
《java並行程式設計實戰》
https://www.cnblogs.com/czwbig/p/11127124.html
https://www.cnblogs.com/jelly12345/p/14609657.html
https://www.cnblogs.com/bailiyi/p/11967396.html
本篇文章到這裡就結束啦,很感謝你能看到最後,如果覺得文章對你有幫助,別忘記關注我!更多精彩的文章