JVM學習筆記——java內存模型與線程(1)

概述

多任務處理出現的重要原因是計算機的運算速度與存儲及通信子系統的速度差距太大,大量的時間花費在磁盤I/O,數據庫訪問或者數據庫訪問上。除了充分利用計算機處理器的能力外,一個服務端同時對多個客戶端提供服務則是另一個更具體的併發應用場景,對於計算量相同的,程序併發協調的越有條不紊,效率自然就高,反之線程之間頻繁的阻塞甚至死鎖,將會大大降低程序的併發能力。

硬件的效率以及一致性

由於計算機的處理器的運算速度與存儲設備的讀寫速度存在幾個數量級的差距,所以加入高速緩存作爲緩衝,雖然這很好的解決了處理器和內存之間的速度矛盾,但也帶來了更高的複雜度,帶來了一個新問題:緩存一致性。
在多處理器系統中,每個處理器都有自己的高速緩存,但是又共享同一塊主內存,當多個處理器的運算任務都涉及同一塊主內存區域時,可能導致各自的緩存數據不一致,那麼同步回主存,以哪個數據爲準?爲了解決這個問題,需要處理器在訪問主存是遵循一系列協議來進行操作,比如MSI,MESI,MOSI,FireFly,Dragon Protocol等,而標題上的內存模型,可以理解爲在特定的操作協議下,對特定的內存或者告訴緩存進行讀寫訪問的過程抽象,不同架構的機器有不同的內存模型,java虛擬機也有自己的內存模型。
這裏寫圖片描述
除了增加告訴緩存外,處理器會對輸入代碼進行亂序執行優化以充分利用處理器內部的運算單元,處理器會在計算之後將亂序執行的結果重組,保證該結果與亂序執行的結果是一致的,但不保證該程序中每個語句計算的先後順序和輸入代碼的順序一致,故,如果存在一個計算任務的結果依賴於另一個程序的運算結果,那麼順序並不能依賴於代碼的先後順序來保證,相似的,java虛擬機的即使編譯器中也有類似的指令重排序。

java內存模型

java的內存模型是平臺無關的,c/c++直接使用物理硬件與操作系統的內存模型,因此會由於不同機器內存模型的不同,程序運行出現各種各樣的問題。

主內存與工作內存

java內存模型的主要目標是定義各個變量的訪問規則,即虛擬機從內存中取出即將變量放入內存中的底層細節,這裏的變量包括了實例字段,靜態字段和構成數組對象的元素,但不包括局部變量和方法參數,因爲後者是線程私有的。JVM中所有的變量都存儲在主內存中,每條線程還有自己的工作內存,線程的工作內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的所有操作(讀取、 賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量。不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成。
這裏寫圖片描述

內存間交互操作

Java內存模型中定義了以下8種操作來完成,每一種操作都是原子的、 不可再分的。

操作名 作用
lock(鎖定) 作用於主內存的變量,它把一個變量標識爲一條線程獨佔的狀態
unlock(解鎖) 作用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定。
read(讀取) 作用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用
load(載入) 作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中
use(使用) 作用於工作內存的變量,它把工作內存中一個變量的值傳遞給執行引擎
assign(賦值) 作用於工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量
store(存儲) 作用於工作內存的變量,它把工作內存中一個變量的值傳送到主內存中,以便隨後的write操作使用
write(寫入) 作用於主內存的變量,它把store操作從工作內存中得到的變量的值放入主內存的變量中

上述8種基本操作時必須遵循一系列規則,比如要把一個變量從主內存複製到工作內存,那就要順序地執行read和load操作,這樣十分繁瑣,後面將介紹這種定義的一個等效判斷原則——先行發生原則,用來確定一個訪問在併發環境下是否安全。

對於volatile型變量的特殊規則

volatile型變量的規則可以參考這篇這篇文章。
值得注意的一點是,基於volatile變量的運算在併發下並不是安全的,volatile只能保證對單次讀/寫的原子性,如果多個線程同時對某個數據進行讀寫操作,那就很容易讀到髒數據。
不符合以下兩條規則的運算場景中,我們仍然要通過加鎖(使用synchronized或java.util.concurrent中的原子類)來保證原子性:
1. 運算結果並不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。
2. 變量不需要與其他的狀態變量共同參與不變約束。
使
用volatile變量是禁止指令重排序優化的,普通的變量僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操作的順序與程序代碼中的執行順序一致。

Map configOptions;
char[]configText;
//此變量必須定義爲volatile
volatile boolean initialized=false//假設以下代碼在線程A中執行
//模擬讀取配置信息,當讀取完成後將initialized設置爲true以通知其他線程配置可用
configOptions=new HashMap();
configText=readConfigFile(fileName);
processConfigOptions(configText,configOptions);
initialized=true//假設以下代碼在線程B中執行
//等待initialized爲true,代表線程A已經把配置信息初始化完成
while(!initialized){
sleep();
}/
/使用線程A中初始化好的配置信息
doSomethingWithConfig();

如果定義initialized變量時沒有使用volatile修飾,就可能會由於指令重排序的優化,導致位於線程A中最後一句的代碼“initialized=true”被提前執行。這樣在線程B中使用配置信息的代碼就可能出現錯誤,而volatile關鍵字則可以避免此類情況的發生。

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

對於64位的數據類型(long和double),在模型中特別定義了一條相對寬鬆的規定:允許虛擬機將沒有被volatile修飾的64位數據的讀寫操作劃分爲兩次32位的操作來進行,即允許虛擬機實現選擇可以不保證64位數據類型的load、 store、 read和write這4個操作的原子性,這點就是所謂的long和double的非原子性協定(Nonatomic Treatment of double and long Variables)。
但是各種平臺下的商用虛擬機幾乎都選擇把64位數據的讀寫操作作爲原子操作來對待,所以,僅作了解即可。

原子性、 可見性與有序性

Java內存模型是圍繞着在併發過程中如何處理原子性、 可見性和有序性這3個特徵來建立的,
原子性:由Java內存模型來直接保證的原子性變量操作包括read、 load、assign、 use、 store和write,基本數據類型的訪問讀寫是具備原子性的,如果應用場景需要一個更大範圍的原子性保證,Java內存模型還提供了lock和unlock操作來滿足這種需求,synchronized關鍵字就隱式的使用這兩個操作,因此在synchronized塊之間的操作也具備原子性。
可見性:指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。普通變量和volatile變量都通
過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存作爲傳遞媒介的方式來實現可見性的,不同點在於,volatile的特殊規則保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新。
Synchronized和final也實現了可見性,前者是因爲對一個變量執行unlock操作之前,必須先把此變量同步回主內存中,後者則是由於被final修飾的字段在構造器中一旦初始化完成,並且構造器沒有把“this”的引用傳遞出去,那在其他線程中就能看見final字段的值。
有序性:如果在本線程內觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。 前半句是指“線程內表現爲串行的語義”(Within-Thread As-If-Serial Semantics),後半句是指“指令重排序”現象和“工作內存與主內存同步延遲”現象。

先行發生原則

先行發生是Java內存模型中定義的兩項操作之間的偏序關係,如果說操作A先行發生於操作B,其實就是說在發生操作B之前,操作A產生的影響能被操作B觀察到,“影響”包括修改了內存中共享變量的值、 發送了消息、 調用了方法等。
1. Java內存模型存在一些“天然的”先行發生關係,這些先行發生關係無須任何同步器協助就已經存在,可以在編碼中直接使用。
2. 程序次序規則(Program Order Rule):在一個線程內,按照程序代碼順序,書寫在前面的操作先行生於書寫在後面的操作。
3. 管程鎖定規則(Monitor Lock Rule):一個unlock操作先行發生於後面對同一個鎖的lock操作。 這裏必須強調的是同一個鎖,而“後面”是指時間上的先後順序。
4. volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操作先行發生於後面對這個變量的讀操作,這裏的“後面”同樣是指時間上的先後順序。
5. 線程啓動規則(Thread Start Rule):Thread對象的start()方法先行發生於此線程的每一個動作。
6. 線程終止規則(Thread Termination Rule):線程中的所有操作都先行發生於對此線程的終止檢測,我們可以通過Thread.join()方法結束、 Thread.isAlive()的返回值等手段檢測到線程已經終止執行。
7. 線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生於被中斷線程代碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測到是否有中斷髮生。
8. 對象終結規則(Finalizer Rule):一個對象的初始化完成(構造函數執行結束)先行發生於其finalize()方法的開始。
9. 傳遞性(Transitivity):如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出操作A先行發生於操作C的結論。
時間先後順序與先行發生原則之間基本沒有太大的關係,所以我們衡量併發安全問題的時候不要受到時間順序的干擾,一切必須以先行發生原則爲準。

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