標籤:額外 理解 學習 重排序 關係 學習筆記 管程 block rup
除了在硬體上增加告訴緩衝之外,為了使得處理器內部的運算單元能盡量被充分利用,處理器可能會對輸入代碼進行亂序執行(Out-Of-Order Execution)最佳化,處理器會在計算之後將亂序執行的結果重組,保證該結果與順序執行的結果一致,但並不保證程式中各個語句計算的先後順序與輸入代碼中的順序一致,因此,如果存在一個計算任務依賴另外一個計算任務的中間結果,那麼其順序性並不能靠代碼的先後順序來保證。與處理器的亂序最佳化執行類似,Java虛擬機器的即時編譯器中也有類似的指令重排序(Instruction Reorder)最佳化。
Java記憶體模型
Java虛擬機器規範中試圖定義一種Java記憶體模型(Java Memory Model),來屏蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓Java程式在各種平台下都能達到一致的記憶體訪問效果。
主記憶體與工作記憶體
Java記憶體模型的主要目標是定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體中取出變數這樣的底層細節。此處的變數包括了執行個體欄位,靜態欄位,和構成數組對象的元素,但不包括局部變數和方法參數,因為後者是線程私人的,不會被共用也不會存在競爭問題。
Java記憶體模型規定了所有的變數都儲存在主記憶體(Main Memory)中,每條線程還有自己的工作記憶體(Working Memory),線程的工作記憶體中儲存了被該線程使用到的變數的主記憶體副本拷貝,線程對變數的所有操作(讀取、賦值等),都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數。不同的線程之間也不能直接存取對方工作記憶體中的變數,線程間變數值的傳遞均需要通過主記憶體來完成。
記憶體間互動操作
關於主記憶體與工作記憶體之間具體的互動協議,即一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步回主記憶體之類的實現細節,Java記憶體模型中定義了以下8種操作來完成,虛擬機器實現時必須保證下面提及的每一種操作都是原子的不可再分的(對於double和long類型的變數來說,load, store, read和write操作在某些平台上允許有例外):
lock(鎖定):作用於主記憶體的變數,它把一個變數標識為一條線程獨佔的狀態。
unlock(解鎖): 作用於主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他線程鎖定。
read(讀取): 作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到線程的工作記憶體中,以便隨後的load動作使用。
load(載入): 作用於工作記憶體的變數,它把read操作從主記憶體得到的變值放入工作記憶體的變數副本中
use(使用): 作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用到變數的值的位元組碼指令時將會執行這個操作。
assign(賦值): 作用於工作記憶體的變數,它把一個從執行引擎收到的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
store(儲存): 作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的write操作使用。
write(): 作用於主記憶體的變數,它把store操作從工作記憶體中得到的變數的值放入主記憶體的變數中。
如果要把一個變數從主記憶體複製到工作記憶體,那就要 順序的執行read和load操作,如果要把變數從工作記憶體同步回主記憶體,就要順序的執行store和write操作。Java記憶體模型只要求上述兩個操作必須按順序執行,而沒有保證時連續的。如對主記憶體中a、b進行訪問時,一種可能出現的順序是read a, read b, load b, load a.
Volatile型變數的特殊規則
當一個變數定義位volatile之後,將具備兩種特性,第一是保證此變數對所有線程的可見度,第二是禁止指令重排序最佳化。
可見度
是指當一條線程修改了這個變數的值,新值對於其他線程來說是可以立即得知的。但volatile並不保證i++等非原子操作的並發安全。volatile變數適合與以下兩種情況:
禁止指令重排序最佳化
從硬體架構上講,指令重排序是指CPU採用了允許將多條指令,不按照程式規定的順序分開發送給各相應電路單元處理但並不是說指令任意重排,CPU需要正確處理指示依賴情況以保障程式能得出正確的執行結果。比如指令1把地址A中的值加10,指令2把地址A中的值乘2,指令3把地址B中的值減3,這時指令1和指令2是有依賴的,它們之間的順序不能重排,但指令3可以重排到指令1、2之前或者中間。
原子性、可見度與有序性原子性
Atomicity, 基礎資料型別 (Elementary Data Type)的訪問讀寫是具備原子性的,long和double是例外,因為其是64位的但是絕大多數虛擬機器都會對其進行原子性實現,所以基本不用考慮它們非原子性的情況。
可見度
Visibility, 指當一個線程修改了共用變數的值,其他線程能夠立即得知這個修改。volatile, synchronized和final都能實現可見度。final關鍵字的可見度是指,被final修飾的欄位在構造器中一旦初始化完成,那在其他線程中就能看見final欄位的值。
有序性
Ordering, Java程式中天然的有序性可以總結為一句話:如果在本線程內觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。後半句指的是“指令重排序”和“工作記憶體與主記憶體同步延遲”現象。
先行發生原則
happens-before,下面是Java記憶體模型的天然先行發生關係,可以在編碼中直接使用。如果兩個操作間的關係不在此列或者無法從以下規則中推匯出來,虛擬機器對它們可以隨意的進行重排序。
程式次序規則,Program Order Rule, 在一個線程內,按照程式碼順序,書寫在前面的操作先行發生與書寫在後面的操作。
管程鎖定規則, Monitor Lock Rule, 一個unlock操作先行發生於後面對同一個鎖的lock操作。
volatile變數規則,Volatile variable Rule, 對一個volatile變數的寫操作先行發生於後面對這個變數的讀操作。
線程啟動規則,Thread Star Rule, Thread對象的start()方法先行發生於此線程的每一個動作。
線程終止規則,Thead Termination Rule, 線程中的所有操作都先行發生於對此線程的終止檢測。可以通過Thread.join()方法的結束和Thread.isAlive()的傳回值等手段檢測到線程已終止執行。
線程中斷規則,Thread Interruption Rule, 對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測到是否有中斷髮生。
對象終結規則,Finalizer Rule, 一個對象的初始化完成(建構函式執行結束)先行發生於它的finalize()方法的開始。
傳遞性,Transitivity, 如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出操作A先行發生於操作C的結論。
安全執行緒安全執行緒程度
按照安全執行緒的安全程度由強至弱來排序,我們可以將Java語言中各種操作分享的資料分為以下5類:不可變、絕對安全執行緒、相對安全執行緒、線程相容和線程對立。
不可變
Immutable的對象一定是安全執行緒的,無論是對象的方法實現還是方法的調用者,都不需要再採取任何的安全執行緒保障措施。如果共用資料是一個基礎資料型別 (Elementary Data Type),那麼只要在定義時使用final關鍵字修飾它就可以保證它是不變的;如果共用資料是一個對象,那就需要保證對象的行為不會對其狀態產生任何影響才行。
絕對安全執行緒
一個類不管運行時環境如何,調用者都不需要任何額外的同步措施。這通常需要付出很大甚至有時候是不切實際的代價。在Java API中標註自己是安全執行緒的類,大多都不是絕對的安全執行緒。如多個線程對一個Vector同時添加刪除和讀取,就可能會遇到ArrayIndexOutOfBoundsException.
相對安全執行緒
這是我們通常意義上所講的安全執行緒,它需要保證對這個對象單獨的操作是安全執行緒的,我們在調用的時候不需要做額外的保障措施,但是對於一些特定順序的連續調用,就可能需要在調用端使用額外的同步手段來保證調用的正確性。Java中大部分安全執行緒類都屬於這種類型。
線程相容
對象本身並不是安全執行緒的,但是可以通過在調用端正確的使用同步手段來保證對象在並發環境中可以安全地使用。Java中大部分類都屬於線程相容的。
線程對立
無論調用端是否採取了同步措施,都無法在多線程環境中並發使用的代碼。非常少見並且是有害的。
安全執行緒的實現方法互斥同步
(Mutual Exclusion & Synchronization)是常見的一種並發正確性保障手段。同步是指在多個線程並發訪問共用資料時,保證共用資料在同一個時刻只被一個(或者是一些,使用訊號量的時候)線程使用。互斥是實現同步的一種手段,臨界區(Critical Section),互斥量(Mutex)和訊號量(Semaphore)都是主要的互斥實現方式。
Java的線程是映射到作業系統的原生系統之上的,如果要阻塞或者喚醒一個線程,都需要作業系統來完成,這就需要從使用者態轉換到核心態中,因此狀態轉換需要耗費很多的處理器時間。所以synchronized是一個重量級的操作。ReentrantLock與synchronized很相似,但增加一些進階功能:等待可中斷,公平鎖,鎖可綁定多個條件。公平鎖指多個線程在等待同一個鎖的時,必須按照申請鎖的時間順序來一次獲得鎖,非公平鎖不保證這一點。synchronized能夠實現需求的時候推薦使用synchronized。
非阻塞同步
互斥同步只要問題是進行線程阻塞喚醒所帶來的效能問題,因此這種同步也稱為阻塞同步(Blocking Synchronization)。從處理問題的方式上說,互斥同步屬於一種悲觀的並發策略,總是認為只要不去做正確的同步措施就肯定會出問題。基於衝突檢測的開放式並行存取策略,就是先進行操作,如果沒有其他線程爭用共用資料,那操作就成功了,如果由衝突,那就再採取其他的補償措施(最常見的補償措施就是不斷地重使,直到成功為止)。這種樂觀的並發策略的許多實現都不需要將線程掛起,因此這種同步操作稱為非阻塞同步(Non-Blocking Synchronization)。
compareAndSwap(CAS指令)在硬體指令層面是一個原子操作。
深入理解Java虛擬機器- 學習筆記 - Java記憶體模型與線程