Java內存模型與線程

Java內存模型與線程

概述

在許多情況下,讓計算機同時去做幾件事,不僅是因爲計算機的運算能力強大了,還有一個很重要的原因是計算機的運算速度與它的存儲和通信子系統速度的差距太大,大量的時間花費在磁盤I/O、網絡通信或者數據庫訪問上。

衡量一個服務性能的高低好壞,每秒事務處理數(Transactions Per Second, TPS)是最重要的指標之一,它代表着一秒內服務器端平均能響應的請求總數,而TPS值與程序的併發能力又有非常密切的關係。對於計算量相同的任務,程序線程併發協調得有條不紊,效率自然就會越高;反之,線程間頻繁阻塞甚至死鎖,將會大大降低程序的併發能力。

硬件的效率與一致性

基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,但是也爲計算機系統帶來了更高的複雜度,因爲它引入了一個新的問題:緩存一致性。

處理器可能會對輸入代碼進行亂序執行(Out-Of-Order Execution)優化,處理器會在計算之後將亂序執行的結果重組,保證該結果與順序執行的結果是一致的。

Java虛擬機的即時編譯器中也有類似的指令重排序(Instruction Reorder)優化。

Java內存模型

Java虛擬機規範中試圖定義一種Java內存模型來屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致性的內存訪問效果。

線程的工作內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行。

主內存與工作內存

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

Java內存模型規定了所有的變量都存儲在主內存(Main Memory)中。每條線程還有自己的工作內存(Working Memory),線程的工作內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的所有操作(讀取,賦值等)都必須是工作內存中進行,而不能直接讀寫主內存中的變量。不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成,線程、主內存、工作內存三者的交互關係如下圖:

線程、主內存、工作內存三者的交互關係

內存間交互操作

一個變量如何從主內存拷貝到工作內存,如何從工作內存同步回主內存之類的實現細節,Java內存模型中定義了以下8種操作來完成。這8種操作都是原子性的、不可再分的(對double和Long類型除外)。

  1. lock(鎖定):作用於主內存變量,它把一個變量標識爲一條線程獨佔的狀態。
  2. unlock(解鎖):作用於主內存變量,它把一個處理鎖定的狀態的變量釋放出來,釋放後的變量纔可以被其它線程鎖定,unlock之前必須將變量值同步回主內存。
  3. read(讀取):作用於主內存變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用。
  4. load(載入):作用於工作內存變量,它把read操作從主內存中得到的值放入工作內存的變量副本中。
  5. use(使用):作用於工作內存中的變量,它把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的字節碼指令時將會執行這個操作。
  6. assign(賦值):作用於工作內存變量,它把一個從執行引擎接到的值賦值給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。
  7. store(存儲):作用於工作內存的變量,它把工作內存中一個變量的值傳送到主內存中,以便隨後的write操作使用。
  8. write(寫入):作用於主內存的變量,它把store操作從工作內存中得到的值放入主內存的變量中。

如果要把一個變量從主內存複製到工作內存,那就要順序地執行read和load操作,如果要把變量從工作內存同步回主內存,就要順序地執行store和write操作。Java內存模型只是要求上述兩個操作必須按順序執行,而沒有保證必須是連續執行。也就是說read與load之間、store與write之間是可以插入其它指令的,如果對主在內中的變量a,b進行訪問時,一種可能出現的順序是read a、readb、loadb、load a。除此之外,Java內存模型還規定了執行上述八種基礎操作時必須滿足如下規則:

  1. 不允許read和load、store和write操作之一單獨出現,即不允許一個變量從主內存讀取了但工作內存不接受,或者從工作內存發起回寫但主內存不接受的情況出現。
  2. 不允許一個線程丟棄它的最近的assign操作,即變量在工作內存中改變(爲工作內存變量賦值)了之後必須把該變化同步回主內存。
  3. 不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作內存同步到主內存中。
  4. 一個新變量只能在主內存中“誕生”,不允許在工作內存中直接使用一個未被初始化(load和assign)的變量,換話說就是一個變量在實施use和store操作之前,必須先執行過了assign和load操作。
  5. 一個變量在同一時刻只允許一個線程對其進行lock操作,但是lock操作可以被同一線程執行多次lock,執行多次lock後,在執行相同次數的unlock操作,變量纔會被釋放。
  6. 如果一個變量事先沒有被load操作鎖定,則不允許對它執行unlock操作:也不允許去unlock一個被其它線程鎖定的變量。
  7. 對一個變量執行unloack之前,必須把此變量同步回主內存中(執行store和write操作)

對於volatile型變量的特殊規則

關鍵字volatile可以說是Java虛擬機提供的最輕量級的同步機制,但是它並不容易完全被正確、完整地理解,以至於許多程序員都習慣不使用它,遇到競爭問題的時候一律使用synchronized來進行同步。

當一個變量定義成volatile之後,它將具備兩種特性:第一是保證此變量對所有線程的可見性,這裏的“可見性”是指當一條線程修改了這個變量的值,新值對於其它線程是可以立即得知的。普通變量不能做到這一點,普通變量的值在線程間傳遞均需要通過主內存來完成,如:線程A修改一個普通變量的值,然後向主內存進行回寫,另外一條線程B在線程A回寫完成了之後再從主內存進行讀取操作,新變量的值纔會對線程B可見。

關於volatile變量的可見性,很多人誤以爲以下描述成立:“volatile對所有線程是立即可見的,對volatile變量所有的寫操作都能立即返回到其它線程之中,換句話說,volatile變量在各個線程中是一致的,所以基於volatile變量的運算在併發下是安全的”。這句話的論據部分並沒有錯,但是其論據並不能得出“基於基於volatile變量的運算在併發下是安全的”這個結論。volatile變量在各個線程的工作內存中不存在一致性問題(在各個線程的工作內存中volatile變量也可以存在不一致的情況,但由於每次使用之前都要先刷新,執行引擎看不到不致的情況,因此可以認爲不存在一致性問題),但是Java裏的運算並非原子操作,導致volatile變量的運算在併發下一樣是不安全的。

由於volatile變量只能保證可見性,在不符合以下條件規則的去處場景中,仍然需要通過加鎖(synchronized或java.util.concurrent中的原子類)來保證原子性。

  1. 運算結果不依賴變量的當前值,或者能確保只有單一的線程改變變量的值。
  2. 變量不需要與其它的狀態變量共同參與不變約束。

使用volatile變量的第二個語義是禁止指令重排序優化,普通的變量僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方能獲取到正確的結果,而不能保證變量的賦值操作的順序與程序代碼中的執行順序一致。因爲在一個線程的方法執行過程中無法感知到這一點,這也就是Java內存模型中描述的所謂的”線程內表現爲串行的語義“(Within-Thread As-If-Serial Sematics)。

Map configOptions;  
char[] configText;  
//此變量必須定義爲volatile  
volatile boolean initialized = false;  
//假設以下代碼在線程A中執行  
//模擬讀取配置信息,當讀取完成後  
//將initialized設置爲true來通知其它線程配置可用  
configOptions = new HashMap();  
configText = readConfigFile(fileName);  
processConfigOptions(configText, configOptions);  
initialized = true;  

//假設以下代碼在線程B中執行  
//等線程A待initialized爲true,代表線程A已經把配置信息初始化完成  
while(!initialized) {  
    sleep();  
}  
//使用線程A中初始化好的配置信息  
doSomethingWithConfig();

上面爲一段僞代碼,其中描述的場景十分常見,只是我們在處理配置文件時一般不會出現併發而已。如果定義initialized變量時沒有使用volatile修飾,就可能會由於指令重排序的優化,導致位於線程A中最後一句的代碼”initialized = true“被提前執行,這樣在線程B中使用配置信息的代碼就可能出現錯誤,而volatile關鍵字則可以避免此類情況的發生。

lock addl $0x0,(%esp)指令把修改同步到內存時,意味着所有之前的操作都已經執行完成,這樣便形成了“指令重排序無法越過內存屏障”的效果。

volatile變量讀取操作的性能消耗與普通變量幾乎沒有什麼差別,但是寫操作則可能會慢一些,因爲它需要再代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。不過即便如此,大多數場景下volatile的總開銷仍然要比鎖低,我們在volatile與鎖之中選擇的唯一依據僅僅是volatile的語義能否滿足使用場景的需求。

如一個變量的修改不依賴與原值,則這個時候可以使用volatile關鍵字實現先行發生關係。

對於Long和double型變量的特殊規則

對於32位平臺,64位的操作需要分兩步來進行,與主存的同步。所以可能出現“半個變量”的狀態。

在實際開發中,目前各種平臺下的商用虛擬機幾乎都選擇把64位數據的讀寫操作作爲原子操作來對待,因此我們在編碼時一般不需要把用到的long和double變量專門聲明爲volatile。

原子性、可見性與有序性

Java內存模型是圍繞着併發過程中如何處理原子性、可見性、有序性這三個特徵來建立的,下面是這三個特性的實現原理:

原子性(Atomicity)

由Java內存模型來直接保證的原子性變量操作包括read、load、use、assign、store和write六個,大致可以認爲基礎數據類型的訪問和讀寫是具備原子性的。如果應用場景需要一個更大範圍的原子性保證,Java內存模型還提供了lock和unlock操作來滿足這種需求,儘管虛擬機未把lock與unlock操作直接開放給用戶使用,但是卻提供了更高層次的字節碼指令monitorenter和monitorexit來隱匿地使用這兩個操作,這兩個字節碼指令反映到Java代碼中就是同步塊—synchronized關鍵字,因此在synchronized塊之間的操作也具備原子性。

可見性(Visibility)

可見性就是指當一個線程修改了線程共享變量的值,其它線程能夠立即得知這個修改。Java內存模型是通過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存作爲傳遞媒介的方法來實現可見性的,無論是普通變量還是volatile變量都是如此,普通變量與volatile變量的區別是volatile的特殊規則保證了新值能立即同步到主內存,以及每使用前立即從內存刷新。因爲我們可以說volatile保證了線程操作時變量的可見性,而普通變量則不能保證這一點。

除了volatile之外,Java還有兩個關鍵字能實現可見性,它們是synchronized。同步塊的可見性是由“對一個變量執行unlock操作之前,必須先把此變量同步回主內存中(執行store和write操作)”這條規則獲得的,而final關鍵字的可見性是指:被final修飾的字段是構造器一旦初始化完成,並且構造器沒有把“this”引用傳遞出去,那麼在其它線程中就能看見final字段的值。

有序性(Ordering)

Java內存模型中的程序天然有序性可以總結爲一句話:如果在本線程內觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所有操作都是無序的。前半句是指“線程內表現爲串行語義”,後半句是指“指令重排序”現象和“工作內存主主內存同步延遲”現象。

Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性,volatile關鍵字本身就包含了禁止指令重排序的語義,而synchronized則是由“一個變量在同一時刻只允許一條線程對其進行lock操作”這條規則來獲得的,這個規則決定了持有同一個鎖的兩個同步塊只能串行地進入。

先行發生原則

如果Java內存模型中所有的有序性都只靠volatile和synchronized來完成,那麼有一些操作將會變得很囉嗦,但是我們在編寫Java併發代碼的時候並沒有感覺到這一點,這是因爲Java語言中有一個“先行發生”(Happen-Before)的原則。這個原則非常重要,它是判斷數據是否存在競爭,線程是否安全的主要依賴。

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

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

  1. 程序次序規則(Pragram Order Rule):在一個線程內,按照程序代碼順序,書寫在前面的操作先行發生於書寫在後面的操作。準確地說應該是控制流順序而不是程序代碼順序,因爲要考慮分支、循環結構。
  2. 管程鎖定規則(Monitor Lock Rule):一個unlock操作先行發生於後面對同一個鎖的lock操作。這裏必須強調的是同一個鎖,而”後面“是指時間上的先後順序。
  3. volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操作先行發生於後面對這個變量的讀取操作,這裏的”後面“同樣指時間上的先後順序。
  4. 線程啓動規則(Thread Start Rule):Thread對象的start()方法先行發生於此線程的每一個動作。
  5. 線程終止規則(Thread Termination Rule):線程中的所有操作都先行發生於對此線程的終止檢測,我們可以通過Thread.join()方法結束,Thread.isAlive()的返回值等作段檢測到線程已經終止執行。
  6. 線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測是否有中斷髮生。
  7. 對象終結規則(Finalizer Rule):一個對象初始化完成(構造方法執行完成)先行發生於它的finalize()方法的開始。
  8. 傳遞性(Transitivity):如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出操作A先行發生於操作C的結論。

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

Java與線程

併發不一定要依賴多線程(如PHP中很常見的多進程併發),但在Java中,談論併發,大多數都與線程脫不開關係。

線程的實現

線程時CPU的最小調度單位。

Thread類與大部分的Java API有顯著的差別,它的所有關鍵方法都是聲明爲Native的。一個Native方法往往意味着這個方法沒有使用或無法使用平臺無關的手段來實現。

實現線程主要有3種方式:

  1. 使用內核線程實現
  2. 使用用戶線程實現
  3. 使用用戶線程加輕量級進程混合實現。

Java線程調度

分爲協同式線程調度和搶佔式線程調度。

線程優先級並不是太靠譜,原因是Java的線程是通過映射到系統的原生線程上來實現的,所以線程調度最終還是取決於操作系統。

狀態轉換

狀態轉換

以下方法會讓線程陷入無限期的等待狀態:

  • 沒有設置Timeout參數的Object.wait()方法
  • 沒有設置Timeout參數的Thread.join()方法
  • LockSupport.park()方法

以下方法會讓線程進入限期等待狀體:

  • Thread.sleep()
  • 設置了Timeout參數的Object.wait()方法
  • 設置了Timeout參數的Thread.join()方法
  • LockSupport.parkNanos()方法
  • LockSupport.parkUntil()方法

摘自:
深入理解Java虛擬機
備註:
轉載請註明出處:http://blog.csdn.net/wsyw126/article/details/62545755
作者:WSYW126

發佈了170 篇原創文章 · 獲贊 50 · 訪問量 28萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章