1. 程式人生 > >java內存模型(Java Memory Model)

java內存模型(Java Memory Model)

ble watermark 了解 計算機 als stack lin 方法 兩個


內容導航:

  • Java內存模型
  • 硬件存儲體系結構
  • Java內存模型和硬件存儲體系之間的橋梁:
  1. 共享對象的可見性
  2. 競爭條件

Java內存模型規定了JVM怎樣與計算機存儲系統(RAM)協調工作。JVM是一個虛擬機模型,因此這個模型自然包含一個內存的模型

理解java內存模型對於設計正確的並發程序非常重要。JVM規定了不同線程何時以及怎樣能看到那些被共享變量的讀寫,怎樣同步對共享變量的訪問控制。

最初的java內存模型並不完好。所以他在java1.5中被改動了。以下的內存模型在java1.8中仍然使用。

Java內存模型

JVM中內存模型被劃分為棧(stack)和堆(heap)。下圖是內存的邏輯圖

技術分享

技術分享

在JVM中運行的每個線程有自己的棧空間。

每個線程棧包括有線程調用方法當前運行位置的指針。我們把它叫做棧指針。當線程運行他的代碼時候,棧指針會改變。共享一塊堆空間。

線程棧也包括全部正在被運行的方法的局部變量。一個線程僅僅能訪問他自己的棧。每個線程創建的局部變量對其它線程是不可見的。縱使兩個線程正在運行同一段代碼,他們也僅僅會創建屬於自己棧空間的局部變量。也就是說每個線程有他自己的局部變量,相互之間不影響。

全部的局部基本數據類型(boolean、byte、short、char、int、long、float、double)全然存儲在線程棧空間裏,而且其它線程不可見。一個線程能夠傳遞自己局部基本數據的拷貝給還有一個線程,可是他們之間並不共享。

堆空間中存放的是應用程序中全部的對象,無論是哪一個線程創建的。

而且包含基本數據對象(eg.Byte,Integer,Long等)。

無論這個對象是作為局部變量被創建,還是作為還有一個對象的成員變量被創建。他都在堆中。即一切對象都在堆空間。

看下圖:

技術分享

技術分享

一個局部變量可能是基本數據類型。還可能是一個對象的引用。他們都在創建自己的線程棧空間,引用所指向的真正的對象在堆空間。

一個對象的成員方法。而且這些方法中也可能包括局部變量。這些局部變量在所屬的線程棧中

一個對象的成員變量,無論是基本類型還是對象的引用。都存在這個堆空間的對象的內部

同一個對象能夠被不同的擁有這個對象的引用的線程訪問。而且能夠訪問對象的成員變量。

假設兩個線程同一時刻調用同樣對象的成員方法,他們都能訪問對象的成員變量,可是各自有自己的局部變量的拷貝。

以下的圖闡述了上述內容:

技術分享

技術分享

硬件存儲體系結構:

以下是簡單的現代計算機的存儲結構:

技術分享技術分享

現代計算機通常有2個甚至很多其它的CPU,這些CPU中的一些還可能是多核的。

關鍵點就是多CPU的計算機同意多個線程同一時候執行(是真正的並行而非並發)。每個cpu在給定的時間片執行一個線程。假設java程序是多線程的,每個線程就能夠在各個cpu上同一時候執行。

每一個cpu都包括有一系列寄存器。

cpu直接從寄存器運行運算比從內存要快非常多倍。

每個cpu還可能有cpu緩存(我們熟稱的cache)。實際上大多數現代cpu都有不同容量的cache。Cpu從cache存取又比從內存快,可是比寄存器又慢一些。一些cpu還有多級緩存(L1、L2等)。可是這並不影響理解java內存模型,我們所要知道的就是在內存和cpu之間另一層緩存cache。

通常,cpu訪問主存的時候須要讀一部分到cache。還可能把cache的一部分有讀到寄存器,然後運行運算。當cpu須要把運算結果寫回到主存的時候,會先把值從寄存器更新到cache,cache放滿後再寫回主存。

存儲在cache中的值通常在cpu須要存儲其它的東西時一起被寫回到內存。

Cache把裏面存儲的內容一次性寫回到主存。這個過程並不一定讀寫整個cache。通常情況下是更新cache中更小的單位,叫做cache lines 緩存行。一行或多行cache line會從主存讀取到cache。或者從cache被寫回主存。

Java內存模型和硬件存儲體系之間的橋梁

正如已經提及的,java內存模型和硬件存儲體系是不同的。

硬件存儲體系並不會區分堆和棧。

在硬件層面上,堆和棧都分配在主存中。堆和棧中的一部分還可能在cache和寄存器中。例如以下圖:

技術分享技術分享

當對象和變量被存儲在計算機不同的存儲區域(rejister、cache、main-memory)的時候,就會出現故障了。

有兩個問題例如以下:

  • 線程對共享變量讀寫的可見性
  • 讀寫檢查共享變量的競爭條件

共享對象的可見性

假設兩個或者多個線程共享一個對象而沒實用volatile聲明或者synchronize同步,某一個線程對共享對象的更新對其它線程可能是不可見的。

設想一下一個開始被存在主內存的共享對象。執行在cpu1上的線程把這個共享對象讀入到cache。

然後對這個共享對象做更改。

僅僅要cache還沒有被刷新到主存,對象的更改版本號對執行在其它cpu的線程就是不可見的。這樣一來,每一個線程就使用的是自己對共享對象的拷貝,這些拷貝存儲在各自cpu cache中。

以下這幅圖闡述了這樣的情況。

一個執行在左側cpu的線程把共享對象復制到自己的cpu cache中,而且把count值改為2.這個改變對執行在右側cpu的線程是不可見的。

由於對count的更新並沒有刷新到主存。

技術分享

要解決問題。我們可以使用java的volatilekeyword。Volatilekeyword可以確保被聲明的變量直接從主存讀取或者是直接在主存更新而不經過cache中間層。

競爭條件

多個線程對共享變量的更新還會引發競爭。

設想線程A讀取共享變量的count字段到他的cache,線程B也是。如今線程A對count加1,線程B也是。

如今var1已經被添加兩次,每一個cpu一次。

假設這些添加是被順序運行(先後次序:即A read increament writeback --> B read increament writeback)的,那麽變量count將會順序加1兩次。終於結果是原始值加2寫回主存。

可是,假設兩次添加被並發運行(交叉次序)而沒有適當的同步。那麽無論是A還是B寫回到主存的結果都是原始值加1。雖然做了兩次加。

以下是上述問題的圖:

技術分享技術分享

為了解決問題,引入java的synchronized block,讓某一系列操作成為原子性的,即不能夠被打斷(類似數據庫中的事務transaction),從而實現不同線程對某一代碼塊的相互排斥訪問。

翻譯原文地址:http://tutorials.jenkov.com/java-concurrency/java-memory-model.html

java內存模型(Java Memory Model)