標籤:
工作記憶體與主記憶體
Java記憶體模型(jmm)的出現是為了各種作業系統和硬體的記憶體訪問的差異。
Java記憶體模型規定了變數(不含局部變數,因為局部變數線程私人,不存在共用問題)都得存放在主記憶體中,而每個線程對這些變數的操作都必須是從主記憶體中取出來並在工作記憶體中完成(如讀取、寫入的操作),不同線程之間不能訪問對方的工作記憶體。如,展現了線程、主記憶體、工作記憶體之間的互動關係:
加入一個工作記憶體的目的很明顯,就是為了加快在記憶體中的操作資料的速度,因為工作記憶體優先儲存在寄存器和快取中,這兩個操作的速度都遠遠快於主記憶體。
主記憶體和工作記憶體之間互動的主要操作為:
l Lock(鎖定):作用於主記憶體的變數,將主記憶體該變數標記成當前線程私人的,其他線程無法訪問。
l Unlock(解鎖):作用於主記憶體的變數,解除主記憶體中該變數的鎖定狀態,讓他變成線程共用變數。
l Read(讀取):作用於主記憶體的變數,將該變數讀取到當前線程的工作記憶體中,以便進行load操作。
l Load(載入):作用於工作記憶體中的變數,將read擷取到的變數載入工作記憶體的變數副本中。
l Use(使用):作用於工作記憶體中的變數,虛擬機器執行引擎在執行位元組碼指令的時候,碰到了一個變數就會執行該操作,使用該變數。
l Assgin(賦值):作用於工作記憶體中的變數,虛擬機器執行引擎在執行位元組碼指令的時候,碰到了變數賦值的指令就會執行該操作。
l Store(儲存):作用於工作記憶體中的變數,將工作記憶體中的變數放入主記憶體,以便進行write操作。
l Write(寫入):作用於主記憶體中的變數,將store得到的變數放入主記憶體的變數中。
很明顯,如果兩個線程A、B訪問同一個變數2的時候,都沒有鎖定。
A在工作記憶體改了變數值+1,而B此時並不能看到這個操作,B還以為沒人改動這個值,就認為自己是再原來的值上進行操作-1。算出來的值變成了多少呢?噢,並不知道,因為可能是3,也可能是1。這時,就出現了並發訪問的安全執行緒問題。
Volatile變數
Volatile是jvm提供的最輕量級的同步機制,但是要正確使用不容易。
Volatile可以保證兩個線程對變數操作的可見度,即一個線程操作的操作可以被另一線程看到。但是如果操作不是原子的,依然沒法保證volatile同步的正確性。只有在下述情況,才可以使用這個關鍵字:
l 對變數的寫入操作不依賴於該變數的當前值(比如a=0;a=a+1的操作,整個流程為a初始化為0,將a的值在0的基礎之上加1,然後賦值給a本身,很明顯依賴了當前值),或者確保只有單一線程修改變數。
l 該變數不會與其他狀態變數納入不變性條件中。(當變數本身是不可變時,volatile能保證安全訪問,比如雙重判斷的單例模式。但一旦其他狀態變數參雜進來的時候,並發情況就無法預知,正確性也無法保障)
package org.project.loda.test;/** * * @ClassName: Singleton * @Description: 基於雙重判斷的單例模式(廣泛使用) * @author minjun* @date 2015年6月5日 下午10:20:37 * */public class Singleton {private static volatile Singleton s;public static Singleton getInstance() {if (s == null) {synchronized (Singleton.class) {if (s == null) {s = new Singleton();}}}return s;}}
Volatile還有個特性就是,他可以禁止指令進行重排序最佳化。什麼是重排序?看看java並發編程實戰中的解釋:
各種操作延遲或者看似亂序執行的不同原因,都可以歸為重排序。
這是一個沒有間接性解釋,因為直接解釋很難以理解。下面看看這個程式:
如果按上到下順序判斷,線程one執行順序為:a=1,x=b,線程other執行順序應該為:b=1,y=a。然後完成計算,列印其值。但是,事實可能是:
我的天哪?x=b竟然在a=1之前執行了!!!發生了什嗎?其實,這就是底層最佳化多線程程式的時候進行了重排序操作,導致亂序出現。最可怕的是,這還不是必然的,他可能發生,可能不發生,你根本預料不到!
所以,volatile本身強大的地方就是他還能預防這種情況發生,雖然犧牲了一點效能,但是大大增強了程式的可靠性。但是記住,不要依賴於volatile,在合適的時候才使用他(上文已經說明),如果情況不合適,就使用傳統的synchronized關鍵字同步共用變數的訪問,用來保證程式正確性(這個關鍵字的效能會隨著jvm不斷完善而不斷提升,將來效能會慢慢逼近volatile)。
Happens-Before原則
說了這麼多,發現可以使用volatile和synchronized關鍵字進行同步。但是,你不會無緣無故就使用它們,存在競爭、安全執行緒問題的時候才應該考慮使用,但是如何判斷是否存在這些問題呢?下面介紹java記憶體模型中的一個重點原則——先行發生原則(Happens-Before),使用這個原則作為依據,來指導你判斷是否存線上程安全和競爭問題。
l 程式順序規則:在程式中,如果A操作在B操作之前(比如A代碼在B代碼上面,或者由A程式調用B程式),那麼在這個線程中,A操作將在B操作之前執行。
l 管理鎖定規則:一個unlock操作先於後面對同一個鎖的lock操作之前執行。
l Volatile變數規則:對一個volatile變數的寫操作必須在對該變數的讀操作之前發生。
l 線程啟動規則:線程的Thread.start必須在該線程所有其他動作之前發生
l 線程終止規則:線程中所有操作都先行發生於該線程的終止檢測。可以通過Thread.join()方法結束、Thread.isAlive()的傳回值判斷線程是否終止。
l 線程中斷規則:對線程interrupt()方法的調用必須在被中斷線程的代碼檢測到interrupt調用之前執行。
l 對象終結規則:對象的初始化(建構函式的調用)必須在該對象的finalize()方法完成。
l 傳遞性:如果A先行發生於B,B先行發生於C,那麼A先行發生於C。
這些操作是無需使用任何同步手段就能保證成立的先行發生規則。如果要線程A、B,需要B能看到A操作的結果(無論兩者是否在一個線程當中),需要A、B滿足Happens-Before關係,如果兩個操作不存在Happens-Before關係,JVM會對他們進行任意重排序。當A和B在同一個線程中,或者兩個線程使用同樣的鎖,他們就能滿足Happens-Before,如果使用不同鎖,就不滿足。
線程
線程也叫作輕量級進程,是大多現代作業系統的基本調度單位。在同一個進程中,多個線程共用記憶體空間,因此需要足夠的同步機制才能保證正常訪問。每個線程本身都有各自的程式計數器、棧和局部變數等。在java中使用線程調度的方式是搶佔式的,需要由作業系統分配執行時間,線程本身無法決定(例如java中,只有Thread.yield()可以讓出自己的執行時間,但是並沒有提供可以主動擷取執行時間的操作)。雖然java中線程調度由系統執行,但是還是可以通過設定線程優先順序來“建議”作業系統多給某些線程分配執行時間(然後,這並不一定就能保證高優先順序的先執行,所以不太靠譜...)。
Java定義了如下幾種線程狀態,一個線程有且僅有一個:
指定了幾種狀態以及達到這些狀態所需要經曆的過程,這裡就不給出解釋了,有基礎的同學應該很容易看懂這些狀態。
參考文獻:java並發編程實戰
深入java虛擬機器(JVM進階特性與最佳實務)
Java記憶體模型與線程