最簡單的 Java內存模型 講解



本博客系列是學習並發編程過程中的記錄總結。由於文章比較多,寫的時間也比較散,所以我整理了個目錄貼(傳送門),方便查閱。

並發編程系列博客傳送門


前言

在網上看了很多文章,也看了好幾本書中關於JMM的介紹,我發現JMM確實是Java中比較難以理解的概念。網上很多文章中關於JMM的介紹要么是照搬了一些書上的內容,要么就干脆介紹的就是錯的。本文試着用比較簡潔的語言介紹清楚JMM到底是什么,解決了Java編程中的哪些問題。不求深入,但求讓讀者看地清楚,看完之后能對JMM有個比較直觀的認識。

本文是筆者在總結了網上的多篇文章之后加上自己的理解整理出來的,內容上可能和JMM標准存在偏差,有問題還望留言指出。

什么是JMM

JMM是一個規范,我從JSR113標准中摘錄了一段對JMM的簡單介紹:

JavaTM virtual machines support multiple threads of execution. Threads are represented by the
Thread class. The only way for a user to create a thread is to create an object of this class; each
thread is associated with such an object. A thread will start when the start() method is invoked
on the corresponding Thread object.
The behavior of threads, particularly when not correctly synchronized, can be confusing and
counterintuitive. This specification describes the semantics of multithreaded programs written in
the JavaTM programming language; it includes rules for which values may be seen by a read of
shared memory that is updated by multiple threads. As the specification is similar to the memory
models for different hardware architectures, these semantics are referred to as the JavaTM memory
model.
These semantics do not describe how a multithreaded program should be executed. Rather,
they describe the behaviors that multithreaded programs are allowed to exhibit. Any execution
strategy that generates only allowed behaviors is an acceptable execution strategy.

上面的英文簡要翻譯如下:

Java虛擬機支持多線程執行。在Java中Thread類代表線程,創建一個線程的唯一方法就是創建一個Thread類的實例對象。當調用了對象的start方法后,相應的線程將會執行。

線程的行為有時會令人困惑而且和我們的直覺相左,特別是在線程沒有正確同步的情況下。本規范描述了JVM平台上多線程程序的語義(含義),具體包括一個線程對共享變量的寫入何時能被其他線程“看到”。由於本規范和不同硬件平台上的內存模型相似,所以將本規范命名為Java內存模型。

從上面這段英文介紹中我們可以得到關於JMM的簡要信息:

  • JMM是一個和多線程相關的規范;
  • JMM描述了JVM平台上多線程程序的語義(含義),具體包括一個線程對共享變量的寫入何時能被其他線程“看到”。

但是只看上面對於JMM的簡單解釋,我相信大多數人還是會很暈,對JMM具體是什么還是很模糊。

不過我在上面的這段介紹中又發現了一段對JMM介紹的關鍵信息:

As the specification is similar to the memory models for different hardware architectures, these semantics are referred to as the JavaTM memory model. (JMM和硬件平台上的內存模型相似)

上面的介紹中提到JMM和硬件平台上的內存模型相似,那么我們就先看看硬件平台上的內存模型究竟是什么?

內存模型

有點計算機基礎的同學都應該知道,程序執行的時候其實就是一條條指令在CPU上執行的過程,而指令的執行又勢必會涉及到數據的讀取和寫入。說到數據,就又不得不提到一個重要的硬件:內存。在計算機中,內存是數據的“收集站”,數據從鍵盤、網絡、文件也有可能是一些傳感器設備進入到內存,然后CPU從內存中讀取這些數據並對這些數據進行“加工”后再寫回到內存。

上面整個過程看起來很完美,但是就像人與人之間是有差別的一樣,硬件和硬件之間也存在差別。CPU的運行速度就和尤塞恩·博爾特的速度一樣(飛一樣的速度),而內存的運行速度和CPU相比就像我的跑步速度和博爾特比一樣,根本不是一個數量級的。CPU和內存運行速度的差距會導致整個系統性能的下降,因為CPU每次讀寫數據都要等待內存。(木桶理論在計算機中的體現)

但是這個問題根本就難不倒我們偉大的硬件工程師們。“聰明”的工程師們在CPU中加入了一層CPU高速緩存層。這個緩存的運算速度和CPU相當,當指令在CPU上運行的時候,會先將運算需要的數據從內存中復制一份到CPU的高速緩存當中,那么CPU進行計算時就可以直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束之后,再將高速緩存中的數據刷新到主存當中。(現代CPU其實是有多級緩存的,但是為了簡單起見就沒介紹了,因為我覺得這里不介紹CPU多級緩存不會影響對JMM的理解)

世界好像又重歸於平靜,一切又顯得那么美好。但是其實問題才剛剛開始。

原子性問題

上面提到CPU進行運算時需要將共享變量先加載到CPU緩存中,運算結束后再將最新數據寫回共享內存。這種看起來完美的工作方式其實存在一個問題,下面我們就以上面的圖片為列子,說下這個問題。

假如現在系統環境是 單核CPU+多線程工作模式,共享變量初始值是1,線程1和線程2分別對這個共享變量進行加一操作,理論上這個共享變量最后的值是3。我們看看程序的執行行為是否會和我們預期的一致。

線程對一個共享變量加一的過程需要分三步進行:

step1: read共享變量到工作內存
step2:對共享變量+1
step3:將共享變量寫回主內存

但是上面的三個步驟並不是原子操作,也就是說可能會被打斷。現在假如線程1已經執行完了step1,但是這時CPU時間片用完了,線程2獲得執行機會也從內存中加載共享變量的值(此時共享變量的值還是1),最后兩個線程執行完step2和step3之后共享變量的值是2,並不是3。

出現上面問題的原因就是對共享變量的加一操作並不是原子性操作,所謂原子性操作是指一個或多個操作,要么全部執行且在執行過程中不被任何因素打斷,要么全部不執行。在多線程環境下原子性問題可能會造成錯誤的執行結果。

原子性問題是內存模型存在的第一個問題,但是內存模型存在的問題不止這一個。

緩存一致性問題

隨着科技的進步,對CPU的需求越來越高。但是摩爾定律的失效注定單個CPU的性能已經很難再大幅度提升。此時“聰明”的硬件工程師又出場了,他們創造性地將多個CPU集成到一個上,這樣CPU的性能不就能成倍地增長了么。多核CPU的確帶來了CPU性能的提升,但是這卻“害苦”了軟件工程師,因為多核CPU大大提升了多線程編程的難度。

多核CPU進行多線程編程時存在的一個顯著問題就是緩存一致性問題

以上圖為例,在多核CPU多線程環境下,兩個線程對共享變量a進行加1操作。兩個線程都將共享變量a在內存中的值加載到了工作內存中,如上圖所示。但是此時線程2失去了CPU時間片,而線程1還是繼續執行並成功將變量加一。當線程1執行完之后,內存中的值如下圖所示:

我們發現此時線程2中的變量a的值已經是過期的值,並不是變量a最新的值,所以當線程2執行完之后變量a並不是我們想要的值3。這個問題就是多核CPU中緩存一致性問題。

和上面的原子性問題不同,緩存一致性問題只有在多核多線程環境下才會出現,而原子性問題只要是在多線程環境下都可能會出現。

指令重排序問題

所謂的指令重拍是指CPU為了是內部的處理器單元得到充分的應用,可能會對代碼進行亂序執行的行為。這個指令重拍的行為在單線程環境下不會有任何問題,但是在多線程環境下程序就可能出現錯誤的執行結果。

這邊不准備對指令重排進行深入的討論,大家只要知道指令重排序是一種CPU性能優化的行為,而這個行為在多線程環境下可能會導致程序錯誤的執行結果。下邊舉一個簡單的列子說明這個問題:


class Singleton{
    private static Singleton instance = null;
     
    private Singleton() {
         
    }
     
    public static Singleton getInstance() {
        if(instance==null) { // 1
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();  //2
            }
        }
        return instance;
    }
}

上面的代碼是一段實現單列模式的代碼。代碼中的Instance變量沒有用volatile關鍵字修飾的,會導致這樣一個問題:在線程執行到第1行的時候,代碼讀取到instance不為null時,但是instance引用的對象有可能還沒有完成初始化。

造成這種現象主要的原因是重排序。

2處的代碼可以分解成以下幾步

emory = allocate();      // 1:分配對象的內存空間
ctorInstance(memory);  // 2:初始化對象
instance = memory;    // 3:設置instance指向剛分配的內存地址

上面的第2和第3步之間,可能會被重排序。例如:

memory = allocate();  // 1:分配對象的內存空間
instance = memory;  // 3:設置instance指向剛分配的內存地址
                                          // 注意,此時對象還沒有被初始化!
ctorInstance(memory); // 2:初始化對象

此時程序判斷到instance變量是非空的,但是還沒初始化完成。如果立即使用的話會出現“致命”的問題。

通過上面分析我們看到:隨着CPU性能的不斷提升,隨之出現了原子性問題、緩存一致性問題和指令重排序問題。細心的我們會發現這些問題其實是和多線程環境下共享變量訪問的原子性、可見性和有序性問題一一對應的。

內存模型的作用

為了既保證CPU的高效執行,又保證共享內存讀寫的正確性(原子性、可見性和有序性),人們定義了內存模型。內存模型是一個規范,這個規范能保證共享內存讀寫的正確性。

Java內存模型

上面提到內存模型的出現是為了解決共享變量讀寫的原子性、可見性和有序性問題,但是沒有具體講怎么解決的。下面就來看看在Java中的內存模型JMM。

Java內存模型是內存模型在JVM中的體現。這個模型的主要目標是定義程序中各個共享變量的訪問規則,也就是在虛擬機中將變量存儲到內存以及從內存中取出變量這類的底層細節。通過這些規則來規范對內存的讀寫操作,保證了並發場景下的可見性、原子性和有序性。

Java內存模型規定了所有的變量都存儲在主內存中,每條線程還有自己的工作內存,線程的工作內存中保存了該線程中是用到的變量的主內存副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存。不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量的傳遞均需要自己的工作內存和主存之間進行數據同步進行。

而JMM就作用於工作內存和主存之間數據同步過程。他規定了如何做數據同步以及什么時候做數據同步。也就是說Java線程之間的通信由Java內存模型控制, JMM決定一個線程對共享變量的寫入何時對另一個線程可見。

以下是《並發編程的藝術》中對JMM的定義。

PS:Java線程之間的通信由Java內存模型(本文簡稱為JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關系:線程之間的共享變量存儲在主內存(Main Memory)中,每個線程都有一個私有的本地內存(Local Memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存、寫緩沖區、寄存器以及其他的硬件和編譯器優化。

簡單總結

Java的多線程之間是通過共享內存進行通信的,而由於采用共享內存進行通信,在通信過程中會存在一系列如原子性、可見性和有序性的問題。JMM就是為了解決這些問題而出現的,這個模型建立了一些規范,可以保證在多核CPU多線程編程環境下,對共享變量讀寫的原子性、可見性和有序性。

再簡單點說 JMM就是一個為了解決多核CPU多線程編程環境下對共享變量訪問存在原子性、可見性和有序性問題 的規范。

本篇博客只是簡單講了下JMM的概念,以及解決哪些問題。具體JMM怎么解決原子性、可見性和有序性問題的,后續會寫博客分析。

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM