[Java併發編程][EP3][Java內存模型]

[EP3][Java內存模型]

Java內存模型是什麼?

Java內存模型 是Java Memory Model
是JVM爲了Java工程師在不同平臺上可以進行行爲一致的編程(比如C++就要根據不同平臺進行編譯)
而提供的屏蔽了平臺差異的Java專用的內存分配和程序執行集合

Java的內存結構

Java虛擬機在執行Java程序的過程中,會把它管理的內存劃分爲幾個不同的數據區域,這些區域都有各自的用途、生命週期。

在這裏插入圖片描述

Java的內存結構 分爲以下幾個部分

1.PC寄存器/程序計數器:

嚴格來說是一個數據結構,用於保存當前正在執行的程序的內存地址,可以看做是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裏(僅是概念模型,各種虛擬機可能會通過一些更高效的方式去實現),字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。
  由於Java是支持多線程執行的,所以程序執行的軌跡不可能一直都是線性執行。由於Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個內核)只會執行一條線程中的指令。因此,爲了線程切換後能恢復到正確的執行位置,被中斷的線程的程序當前執行到哪條內存地址必然要保存下來,所以每條線程都需要有一個獨立的程序計數器,以便用於被中斷的線程恢復執行時再按照被中斷時的指令地址繼續執行下去,各條線程之間的計數器互不影響,獨立存儲,我們稱這類內存區域爲“線程私有”的內存,這在某種程度上有點類似於“ThreadLocal”,是線程安全的,線程的工作內存包括這部分。

此內存區域是唯一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError情況的區域。

2.Java棧 (Java Stack):

Java棧總是與線程關聯在一起的,每當創建一個線程,JVM就會爲該線程創建對應的Java棧,在這個Java棧中又會包含多個棧幀(Stack Frame),這些棧幀是與每個方法關聯起來的,每運行一個方法就創建一個棧幀,每個棧幀會含有一些局部變量表、操作棧和方法返回值等信息。每一個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程
  局部變量表存放了編譯期可知的各種基本數據類型、對象引用(reference類型,它不等同於對象本身,根據不同的虛擬機實現,它可能是一個指向對象起始地址的引用指針,也可能指向一個代表對象的句柄或者其他與此對象相關的位置)和返回地址類型(指向了一條字節碼指令的地址)。
  每當一個方法執行完成時,該棧幀就會彈出棧幀的元素作爲這個方法的返回值,並且清除這個棧幀,Java棧的棧頂的棧幀就是當前正在執行的活動棧,也就是當前正在執行的方法,PC寄存器也會指向該地址。只有這個活動的棧幀的本地變量可以被操作棧使用,當在這個棧幀中調用另外一個方法時,與之對應的一個新的棧幀被創建,這個新創建的棧幀被放到Java棧的棧頂,變爲當前的活動棧。同樣現在只有這個棧的本地變量才能被使用,當這個棧幀中所有指令都完成時,這個棧幀被移除Java棧,剛纔的那個棧幀變爲活動棧幀,前面棧幀的返回值變爲這個棧幀的操作棧的一個操作數。
  由於Java棧是與線程對應起來的,Java棧數據不是線程共有的,所以不需要關心其數據一致性,也不會存在同步鎖的問題。線程的工作內存也包括這部分。
  在Java虛擬機規範中,對這個區域規定了兩種異常狀況:如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;如果虛擬機可以動態擴展,如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常。在Hot Spot虛擬機中,可以使用-Xss參數來設置棧的大小。棧的大小直接決定了函數調用的可達深度。

                                               ![](https://images2015.cnblogs.com/blog/871886/201701/871886-20170103212909316-559443487.png)

3.堆 Heap:

堆是JVM所管理的內存中國最大的一塊,是被所有Java線程鎖共享的,不是線程安全的,在JVM啓動時創建。堆是存儲Java對象的地方,這一點Java虛擬機規範中描述是:所有的對象實例以及數組都要在堆上分配。Java堆是GC管理的主要區域,從內存回收的角度來看,由於現在GC基本都採用分代收集算法,所以Java堆還可以細分爲:新生代和老年代;新生代再細緻一點有Eden空間、From Survivor空間、To Survivor空間等(在G1 GC中,由於使用了全新的着色指針算法,所以已經沒有了年輕代,老年代的劃分)。根據Java虛擬機規範的規定,Java堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可,就像我們的磁盤空間一樣。在實現時,既可以實現成固定大小的,也可以是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的(通過-Xmx和-Xms控制)。
如果在堆中沒有內存完成實例分配,並且堆也無法再擴展時,將會拋出OutOfMemoryError異常。

4.方法區Method Area:

方法區存放了要加載的類的信息(名稱、修飾符等)、類中的靜態常量、類中定義爲final類型的常量、類中的Field信息、類中的方法信息,當在程序中通過Class對象的getName.isInterface等方法來獲取信息時,這些數據都來源於方法區。方法區是被Java線程鎖共享的,不像Java堆中其他部分一樣會頻繁被GC回收,它存儲的信息相對比較穩定,在一定條件下會被GC,當方法區要使用的內存超過其允許的大小時,會拋出OutOfMemory的錯誤信息。方法區也是堆中的一部分,就是我們通常所說的Java堆中的永久區 Permanet Generation,大小可以通過參數來設置,可以通過-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值。

5.常量池Constant Pool:

常量池本身是方法區中的一個數據結構。常量池中存儲瞭如字符串、final變量值、類名和方法名常量。常量池在編譯期間就被確定,並保存在已編譯的.class文件中。一般分爲兩類:字面量和應用量。字面量就是字符串、final變量等。類名和方法名屬於引用量。引用量最常見的是在調用方法的時候,根據方法名找到方法的引用,並以此定爲到函數體進行函數代碼的執行。引用量包含:類和接口的權限定名、字段的名稱和描述符,方法的名稱和描述符。

6.本地方法棧Native Method Stack:

本地方法棧和Java棧所發揮的作用非常相似,區別不過是Java棧爲JVM執行Java方法服務,而本地方法棧爲JVM執行Native方法服務。本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常。

堆是運行時數據區,垃圾回收主要發生在堆上,
對於堆上的數據,不需要在編譯器預定義數據生存期,因爲堆可以動態的分配數據,缺點是存取速度較棧慢。堆存放所有的實例對象。

棧是存儲其他數據區,Java要求系統調用棧,方法本地變量,基礎類型數據,對象聲明存儲在棧上。
棧的速度比堆快,接近處理器的寄存器速度,但棧上的數據生命週期必須在編譯器預定義好。

多線程下的內存模型

在這裏插入圖片描述
主內存和工作內存:

Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在JVM中將變量存儲到內存和從內存中取出變量這樣的底層細節。此處的變量與Java編程裏面的變量有所不同步,它包含了實例字段、靜態字段和構成數組對象的元素,但不包含局部變量和方法參數,因爲後者是線程私有的,不會共享,當然不存在數據競爭問題(如果局部變量是一個reference引用類型,它引用的對象在Java堆中可被各個線程共享,但是reference引用本身在Java棧的局部變量表中,是線程私有的)。爲了獲得較高的執行效能,Java內存模型並沒有限制執行引起使用處理器的特定寄存器或者緩存來和主內存進行交互,也沒有限制即時編譯器進行調整代碼執行順序這類優化措施。

JMM規定了所有的變量都存儲在主內存(Main Memory)中。每個線程還有自己的工作內存(Working Memory),線程的工作內存中保存了該線程使用到的變量的主內存的副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量(volatile變量仍然有工作內存的拷貝,但是由於它特殊的操作順序性規定,所以看起來如同直接在主內存中讀寫訪問一般)。不同的線程之間也無法直接訪問對方工作內存中的變量,線程之間值的傳遞都需要通過主內存來完成。

Java內存間交互操作的規則

關於主內存與工作內存之間的具體交互協議,即一個變量如何從主內存拷貝到工作內存、如何從工作內存同步到主內存之間的實現細節,Java內存模型定義了以下八種操作來完成

  • lock 鎖定:作用於主內存變量,用於把一個變量標記爲此線程獨佔
  • unlock 解鎖 :作用於主內存變量,用於把一個標記爲此線程獨佔變量的釋放,被釋放的變量纔可以被其他線程加鎖
  • read 讀取:作用於主內存變量,用於把一個變量從主內存傳入線程工作內存 一遍後續載入
  • loda 載入: 作用於主內存變量,用於把一個變量從主內存中讀取的數據存入線程工作內存中的變量副本里
  • use 使用:作用於線程工作內存變量,把線程工作內存中的一個變量值傳遞給執行引擎
  • assign 賦值:作用線程於工作內存變量,用於把執行引擎返回的值存入線程工作內存中的變量
  • store 存儲:作用於線程工作內存變量,用於把線程工作內存中的一個變量傳回主內存
  • write 寫出:作用於主內存,用於把線程工作內存傳回的變量值寫入到主內存的變量中

爲了保證Java多線程併發的正確執行
JVM制定了許多規則 保證有先後關係的指令正確執行
如果要把一個變量從主內存中複製到工作內存,就需要按順尋地執行read和load操作,如果把變量從工作內存中同步回主內存中,就要按順序地執行store和write操作。Java內存模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。也就是read和load之間,store和write之間是可以插入其他指令的,如對主內存中的變量a、b進行訪問時,可能的順序是read a,read b,load b, load a。Java內存模型還規定了在執行上述八種基本操作時,必須滿足如下規則:

  • 不允許線程在沒有給變量賦值的情況下將數據從工作內存同步到主存
  • 變量從主存同步到工作內存,必須按順序執行讀取和載入操作;變量從工作內存同步到主存,必須按順序的執行存儲和寫出操作。但Java只要求這兩個操作必須按順序執行,沒有要求必須連續執行
  • 不允許讀取和載入,存儲和寫出這兩個成對操作單一執行
  • 不允許線程丟棄最近的賦值操作,即變量在線程工作內存中改變後,必須同步回主內存
  • 一個新變量只能在主存中聲明然後使用,不允許在線程工作內存中使用主內存中未初始化的變量。即一個變量進行使用和存儲操作之前,必須先進行賦值和載入
  • 一個變量在同一時刻只允許一條線程對其進行lock操作,lock和unlock必須成對出現
  • 如果對一個變量執行鎖定操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前需要重新執行載入或賦值操作初始化變量的值
  • 如果一個變量事先沒有被鎖定操作鎖定,則不允許對它執行解鎖操作;也不允許去解鎖一個被其他線程鎖定的變量。
  • 對一個變量執行解鎖操作之前,必須先把此變量同步到主內存中(執行存儲和寫出操作)。

指令重排序

在執行程序時爲了提高性能,編譯器和處理器經常會對指令進行重排序。重排序分成三種類型:

  1. 編譯器優化的重排序。編譯器在不改變單線程程序語義放入前提下,可以重新安排語句的執行順序。
  2. 指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
  3. 內存系統的重排序。由於處理器使用緩存和讀寫緩衝區,這使得加載和存儲操作看上去可能是在亂序執行。

從Java源代碼到最終實際執行的指令序列,會經過下面三種重排序:

爲了保證內存的可見性,Java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。Java內存模型把內存屏障分爲LoadLoad、LoadStore、StoreLoad和StoreStore四種:

通過內存屏障 Java虛擬機可以保證有先後關係的程序 可以按順序執行

原子性、可見性與有序性

Java內存模型保證併發編程的安全性,是通過原子性、可見性、有序性這三個原則來確立的。
 原子性(Atomicity):一個操作不能被打斷,要麼全部執行完畢,要麼不執行。在這點上有點類似於事務操作,要麼全部執行成功,要麼回退到執行該操作之前的狀態。

基本類型數據的訪問大都是原子操作,long 和double類型的變量是64位,但是在32位JVM中,32位的JVM會將64位數據的讀寫操作分爲2次32位的讀寫操作來進行,這就導致了long、double類型的變量在32位虛擬機中是非原子操作,數據有可能會被破壞,也就意味着多個線程在併發訪問的時候是線程非安全的。

**可見性:**一個線程對共享變量做了修改之後,其他的線程立即能夠看到(感知到)該變量這種修改(變化)。

Java內存模型是通過將在工作內存中的變量修改後的值同步到主內存,在讀取變量前從主內存刷新最新值到工作內存中,這種依賴主內存的方式來實現可見性的。

無論是普通變量還是volatile變量都是如此,區別在於:volatile的特殊規則保證了volatile變量值修改後的新值立刻同步到主內存,每次使用volatile變量前立即從主內存中刷新,因此volatile保證了多線程之間的操作變量的可見性,而普通變量則不能保證這一點。

除了volatile關鍵字能實現可見性之外,還有synchronized,Lock,final也是可以的。

使用synchronized關鍵字,在同步方法/同步塊開始時(Monitor Enter),使用共享變量時會從主內存中刷新變量值到工作內存中(即從主內存中讀取最新值到線程私有的工作內存中),在同步方法/同步塊結束時(Monitor Exit),會將工作內存中的變量值同步到主內存中去(即將線程私有的工作內存中的值寫入到主內存進行同步)。

使用Lock接口的最常用的實現ReentrantLock(重入鎖)來實現可見性:當我們在方法的開始位置執行lock.lock()方法,這和synchronized開始位置(Monitor Enter)有相同的語義,即使用共享變量時會從主內存中刷新變量值到工作內存中(即從主內存中讀取最新值到線程私有的工作內存中),在方法的最後finally塊裏執行lock.unlock()方法,和synchronized結束位置(Monitor Exit)有相同的語義,即會將工作內存中的變量值同步到主內存中去(即將線程私有的工作內存中的值寫入到主內存進行同步)。

final關鍵字的可見性是指:被final修飾的變量,在構造函數數一旦初始化完成,並且在構造函數中並沒有把“this”的引用傳遞出去(“this”引用逃逸是很危險的,其他的線程很可能通過該引用訪問到只“初始化一半”的對象),那麼其他線程就可以看到final變量的值。

**  有序性:**對於一個線程的代碼而言,我們總是以爲代碼的執行是從前往後的,依次執行的。這麼說不能說完全不對,在單線程程序裏,確實會這樣執行;但是在多線程併發時,程序的執行就有可能出現亂序。用一句話可以總結爲:在本線程內觀察,操作都是有序的;如果在一個線程中觀察另外一個線程,所有的操作都是無序的。前半句是指“線程內表現爲串行語義(WithIn Thread As-if-Serial Semantics)”,後半句是指“指令重排”現象和“工作內存和主內存同步延遲”現象。

Java提供了兩個關鍵字volatile和synchronized來保證多線程之間操作的有序性,volatile關鍵字本身通過加入內存屏障來禁止指令的重排序,而synchronized關鍵字通過一個變量在同一時間只允許有一個線程對其進行加鎖的規則來實現,

在單線程程序中,不會發生“指令重排”和“工作內存和主內存同步延遲”現象,只在多線程程序中出現。

happens-before原則

Java內存模型中定義的兩項操作之間的次序關係,如果說操作A先行發生於操作B,操作A產生的影響能被操作B觀察到,“影響”包含了修改了內存中共享變量的值、發送了消息、調用了方法等。

下面是Java內存模型下一些”天然的“happens-before關係,這些happens-before關係無須任何同步器協助就已經存在,可以在編碼中直接使用。如果兩個操作之間的關係不在此列,並且無法從下列規則推導出來的話,它們就沒有順序性保障,虛擬機可以對它們進行隨意地重排序。

a.程序次序規則(Pragram Order Rule):在一個線程內,按照程序代碼順序,書寫在前面的操作先行發生於書寫在後面的操作。準確地說應該是控制流順序而不是程序代碼順序,因爲要考慮分支、循環結構。

b.管程鎖定規則(Monitor Lock Rule):一個unlock操作先行發生於後面對同一個鎖的lock操作。這裏必須強調的是同一個鎖,而”後面“是指時間上的先後順序。

c.volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操作先行發生於後面對這個變量的讀取操作,這裏的”後面“同樣指時間上的先後順序。

d.線程啓動規則(Thread Start Rule):Thread對象的start()方法先行發生於此線程的每一個動作。

e.線程終於規則(Thread Termination Rule):線程中的所有操作都先行發生於對此線程的終止檢測,我們可以通過Thread.join()方法結束,Thread.isAlive()的返回值等作段檢測到線程已經終止執行。

f.線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測是否有中斷髮生。

g.對象終結規則(Finalizer Rule):一個對象初始化完成(構造方法執行完成)先行發生於它的finalize()方法的開始。

g.傳遞性(Transitivity):如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出操作A先行發生於操作C的結論。

一個操作”時間上的先發生“不代表這個操作會是”先行發生“,那如果一個操作”先行發生“是否就能推導出這個操作必定是”時間上的先發生 “呢?也是不成立的,一個典型的例子就是指令重排序。所以時間上的先後順序與happens-before原則之間基本沒有什麼關係,所以衡量併發安全問題一切必須以happens-before 原則爲準。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章