最簡單的 Java內存模型 講解


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

併發編程系列博客傳送門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性能優化的行爲,而這個行爲在多線程環境下可能會致使程序錯誤的執行結果。

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

內存模型的做用

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

Java內存模型

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

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

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

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

JAVA

以上圖片來自(https://www.hollischuang.com/archives/2550

簡單總結

Java的多線程之間是經過共享內存進行通訊的,而因爲採用共享內存進行通訊,在通訊過程當中會存在一系列如原子性、可見性和有序性的問題。JMM就是爲了解決這些問題而出現的,這個模型創建了一些規範,能夠保證在多核CPU多線程編程環境下,對共享變量讀寫的原子性、可見性和有序性。

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

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

參考

相關文章
相關標籤/搜索