終於有人把Java內存模型說清楚了

內部原理
JVM 中試圖定義一種 JMM 來屏蔽各類硬件和操做系統的內存訪問差別,以實現讓 Java 程序在各類平臺下都能達到一致的內存訪問效果。編程

JMM 的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。此處的變量與 Java 編程中的變量有所區別,它包括了實例字段、靜態字段和構成數組對象的元素,但不包括局部變量與方法參數,由於後者是線程私有的,不會被共享,天然就不會存在競爭問題。爲了得到較好的執行效能,Java 內存模型並無限制執行引擎使用處理器的特定寄存器或緩存來和主存進行交互,也沒有限制即便編譯器進行調整代碼執行順序這類優化措施。數組

JMM 是圍繞着在併發過程當中如何處理原子性、可見性和有序性這 3 個特徵來創建的。緩存

JMM 是經過各類操做來定義的,包括對變量的讀寫操做,監視器的加鎖和釋放操做,以及線程的啓動和合並操做。多線程

內存模型結構
Java 內存模型把 Java 虛擬機內部劃分爲線程棧和堆。架構

線程棧
每個運行在 Java 虛擬機裏的線程都擁有本身的線程棧。這個線程棧包含了這個線程調用的方法當前執行點相關的信息。一個線程僅能訪問本身的線程棧。一個線程建立的本地變量對其它線程不可見,僅本身可見。即便兩個線程執行一樣的代碼,這兩個線程任然在在本身的線程棧中的代碼來建立本地變量。所以,每一個線程擁有每一個本地變量的獨有版本。併發

全部原始類型的本地變量都存放在線程棧上,所以對其它線程不可見。一個線程可能向另外一個線程傳遞一個原始類型變量的拷貝,可是它不能共享這個原始類型變量自身。app


堆上包含在 Java 程序中建立的全部對象,不管是哪個對象建立的。這包括原始類型的對象版本。若是一個對象被建立而後賦值給一個局部變量,或者用來做爲另外一個對象的成員變量,這個對象任然是存放在堆上。機器學習

一個本地變量多是原始類型,在這種狀況下,它老是在線程棧上。
一個本地變量也多是指向一個對象的一個引用。在這種狀況下,引用(這個本地變量)存放在線程棧上,可是對象自己存放在堆上。
一個對象可能包含方法,這些方法可能包含本地變量。這些本地變量任然存放在線程棧上,即便這些方法所屬的對象存放在堆上。
一個對象的成員變量可能隨着這個對象自身存放在堆上。無論這個成員變量是原始類型仍是引用類型。
靜態成員變量跟隨着類定義一塊兒也存放在堆上。
存放在堆上的對象能夠被全部持有對這個對象引用的線程訪問。當一個線程能夠訪問一個對象時,它也能夠訪問這個對象的成員變量。若是兩個線程同時調用同一個對象上的同一個方法,它們將會都訪問這個對象的成員變量,可是每個線程都擁有這個本地變量的私有拷貝。分佈式

clipboard.png

硬件內存架構
現代硬件內存模型與 Java 內存模型有一些不一樣。理解內存模型架構以及 Java 內存模型如何與它協同工做也是很是重要的。這部分描述了通用的硬件內存架構,下面的部分將會描述 Java 內存是如何與它「聯手」工做的。函數

clipboard.png

一個現代計算機一般由兩個或者多個 CPU。其中一些 CPU 還有多核。從這一點能夠看出,在一個有兩個或者多個 CPU 的現代計算機上同時運行多個線程是可能的。每一個 CPU 在某一時刻運行一個線程是沒有問題的。這意味着,若是你的 Java 程序是多線程的,在你的 Java 程序中每一個 CPU 上一個線程可能同時(併發)執行。

每一個 CPU 都包含一系列的寄存器,它們是 CPU 內內存的基礎。CPU 在寄存器上執行操做的速度遠大於在主存上執行的速度。這是由於 CPU 訪問寄存器的速度遠大於主存。

每一個 CPU 可能還有一個 CPU 緩存層。實際上,絕大多數的現代 CPU 都有必定大小的緩存層。CPU 訪問緩存層的速度快於訪問主存的速度,但一般比訪問內部寄存器的速度還要慢一點。一些 CPU 還有多層緩存,但這些對理解 Java 內存模型如何和內存交互不是那麼重要。只要知道 CPU 中能夠有一個緩存層就能夠了。

一個計算機還包含一個主存。全部的 CPU 均可以訪問主存。主存一般比 CPU 中的緩存大得多。

一般狀況下,當一個 CPU 須要讀取主存時,它會將主存的部分讀到 CPU 緩存中。它甚至可能將緩存中的部份內容讀到它的內部寄存器中,而後在寄存器中執行操做。當 CPU 須要將結果寫回到主存中去時,它會將內部寄存器的值刷新到緩存中,而後在某個時間點將值刷新回主存。

當 CPU 須要在緩存層存放一些東西的時候,存放在緩存中的內容一般會被刷新回主存。CPU 緩存能夠在某一時刻將數據局部寫到它的內存中,和在某一時刻局部刷新它的內存。它不會再某一時刻讀/寫整個緩存。一般,在一個被稱做「cache lines」的更小的內存塊中緩存被更新。一個或者多個緩存行可能被讀到緩存,一個或者多個緩存行可能再被刷新回主存。

JMM 和硬件內存架構之間的橋接
上面已經提到,Java 內存模型與硬件內存架構之間存在差別。硬件內存架構沒有區分線程棧和堆。對於硬件,全部的線程棧和堆都分佈在主內中。部分線程棧和堆可能有時候會出如今 CPU 緩存中和 CPU 內部的寄存器中。以下圖所示:

clipboard.png

當對象和變量被存放在計算機中各類不一樣的內存區域中時,就可能會出現一些具體的問題。主要包括以下兩個方面:

線程對共享變量修改的可見性
當讀,寫和檢查共享變量時出現 race conditions

clipboard.png

共享對象可見性
若是兩個或者更多的線程在沒有正確的使用 volatile 聲明或者同步的狀況下共享一個對象,一個線程更新這個共享對象可能對其它線程來講是不接見的。

想象一下,共享對象被初始化在主存中。跑在 CPU 上的一個線程將這個共享對象讀到 CPU 緩存中。而後修改了這個對象。只要 CPU 緩存沒有被刷新會主存,對象修改後的版本對跑在其它 CPU 上的線程都是不可見的。這種方式可能致使每一個線程擁有這個共享對象的私有拷貝,每一個拷貝停留在不一樣的 CPU 緩存中。

上圖示意了這種情形。跑在左邊 CPU 的線程拷貝這個共享對象到它的 CPU 緩存中,而後將 count 變量的值修改成 2。這個修改對跑在右邊 CPU 上的其它線程是不可見的,由於修改後的 count 的值尚未被刷新回主存中去。

解決這個問題你可使用 Java 中的 volatile 關鍵字。volatile 關鍵字能夠保證直接從主存中讀取一個變量,若是這個變量被修改後,老是會被寫回到主存中去。

競態條件
若是兩個或者更多的線程共享一個對象,多個線程在這個共享對象上更新變量,就有可能發生 race conditions。

想象一下,若是線程 A 讀一個共享對象的變量 count 到它的 CPU 緩存中。再想象一下,線程 B 也作了一樣的事情,可是往一個不一樣的 CPU 緩存中。如今線程 A 將 count 加 1,線程 B 也作了一樣的事情。如今 count 已經被增在了兩個,每一個 CPU 緩存中一次。

若是這些增長操做被順序的執行,變量 count 應該被增長兩次,而後原值+2 被寫回到主存中去。

然而,兩次增長都是在沒有適當的同步下併發執行的。不管是線程 A 仍是線程 B 將 count 修改後的版本寫回到主存中取,修改後的值僅會被原值大 1,儘管增長了兩次。

解決這個問題可使用 Java 同步塊。一個同步塊能夠保證在同一時刻僅有一個線程能夠進入代碼的臨界區。同步塊還能夠保證代碼塊中全部被訪問的變量將會從主存中讀入,當線程退出同步代碼塊時,全部被更新的變量都會被刷新回主存中去,無論這個變量是否被聲明爲 volatile。

Happens-Before
JMM 爲程序中全部的操做定義了一個偏序關係,稱之爲 Happens-Before。

程序順序規則:若是程序中操做 A 在操做 B 以前,那麼在線程中操做 A 將在操做 B 以前執行。
監視器鎖規則:在監視器鎖上的解鎖操做必須在同一個監視器鎖上的加鎖操做以前執行。
volatile 變量規則:對 volatile 變量的寫入操做必須在對該變量的讀操做以前執行。
線程啓動規則:在線程上對 Thread.start 的調用必須在該線程中執行任何操做以前執行。
線程結束規則:線程中的任何操做都必須在其餘線程檢測到該線程已經結束以前執行,或者從 Thread.join 中成功返回,或者在調用 Thread.isAlive 時返回 false。
中斷規則:當一個線程在另外一個線程上調用 interrupt 時,必須在被中斷線程檢測到 interrupt 調用以前執行(經過拋出 InterruptException,或者調用 isInterrupted 和 interrupted)。
終結器規則:對象的構造函數必須在啓動該對象的終結器以前執行完成。
傳遞性:若是操做 A 在操做 B 以前執行,而且操做 B 在操做 C 以前執行,那麼操做 A 必須在操做 C 以前執行。
免費Java資料須要本身領取,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo/Kafka、Hadoop、Hbase、Flink等高併發分佈式、大數據、機器學習等技術。
傳送門:https://mp.weixin.qq.com/s/Jz...

相關文章
相關標籤/搜索