深入理解Java虛擬機器讀書筆記8----Java記憶體模型與執行緒
阿新 • • 發佈:2018-12-19
八 Java記憶體模型與執行緒
1 Java記憶體模型
---主要目標:定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體中取出變數這樣的底層細節。
---此處的變數和Java中的變數有所區別,它包括類欄位、例項欄位和構成陣列物件的元素,但不包括區域性變數和方法引數。
---Java記憶體模型規定:
· 所有的變數都儲存在主記憶體中;
· 每條執行緒還有自己的工作記憶體,執行緒的工作記憶體中儲存了被該執行緒使用到的變數的主記憶體副拷貝,執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體中變數;
· 不同的執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數值的傳遞均需要通過主記憶體來完成。
---執行緒、主記憶體、工作記憶體三者的互動關係如下圖:
2 記憶體間互動操作
---Java記憶體模型中定義了以下8種操作,並且每一種操作都是原子的、不可再分的(double、long存在例外):
· lock:作用於主記憶體的變數,把一個變數標識為執行緒獨佔的狀態。
· unlock:作用於主記憶體的變數,把一個處於鎖定狀態的變數釋放出來,釋放後的變數才能被其他執行緒鎖定。
· read:作用於主記憶體的變數,把一個變數的值從主記憶體中傳輸到執行緒的工作記憶體中,以便之後的load使用。
· load:作用於工作記憶體的變數,把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
· use:作用於工作記憶體的變數,把工作記憶體中一個變數的值傳遞給執行引擎。
· assign:作用於工作記憶體的變數,把一個從執行引擎接收到的值賦給工作記憶體的變數。
· store:作用於工作記憶體的變數,把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的write操作使用。
· write:作用於主記憶體的變數,把store操作從工作記憶體中得到的變數的值放入主記憶體的變數中。
---Java記憶體模型還規定了在執行上述8種操作時必須滿足如下規則:
· 不允許read和load、store和write操作之一單獨出現。
· 不允許一個執行緒丟棄它的最近的assign操作,即變數在工作記憶體中改變了之後必須把該變化同步回主記憶體。
· 不允許一個執行緒無原因地(沒有發生過任何地assign操作)把資料從執行緒的工作記憶體同步回主記憶體中。
· 一個新的變數只能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化的變數。
· 一個變數在同一個時刻只允許一條執行緒對其進行lock操作,但lock操作可以被同一條執行緒重複多次執行,多次執行lock後,只有執行相同次數的unlock操作,變數才會被解鎖。
· 如果對一個變數執行lock操作,那將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前,需要重新執行load或assign操作初始化變數的值。
· 如果一個變數事先沒有被lock鎖定,那就不允許對它執行unlock操作,也不允許去unlock一個被其他執行緒鎖定的變數。
· 對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中。
3 volatile變數 ---是Java虛擬機器提供的最輕量級的同步機制; ---volatile變數的特性: · 保證此變數對所有執行緒的可見性。即當一條執行緒修改了這個變數的值,新值對於其他執行緒來說是可以立即得知的; 備註:可見性說明volatile變數在各個執行緒的工作記憶體中不存在一致性問題,但是由於Java裡面的運算並非原子操作,導致volatile變數的運算在併發下一樣是不安全的。 · 禁止指令重排序優化。指令重排序是指CPU採用了允許將多條指令不按程式規定的順序分開發送給各相應電路單元處理,volatile關鍵字保證重排序時不能把後面的指令重排序到記憶體屏障之前的位置。 ---使用volatile必須滿足: · 運算結果並不依賴變數的當前值,或者能夠確保只有單一的執行緒修改變數的值; · 變數不需要與其他的狀態變數共同參與不變約束。 ---效能: · 效能要優於鎖(使用synchronized關鍵字或java.util.concurrent包裡面的鎖); · volatile變數讀操作的效能和普通變數幾乎沒有差別,寫操作可能會慢一些,因為它需要在原生代碼中插入許多記憶體屏障指令來保證處理器不發生亂序執行。 Java記憶體模型對volatile變數定義的特殊規則: · 要求在工作記憶體中,每次使用volatile變數前都必須先從主記憶體重新整理最新的值,用於保證能看見其他執行緒對volatile變數所做的修改後的值; · 要求在工作記憶體中,每次修改volatile變數後都必須立刻同步回主記憶體中,用於保證其他執行緒可以看到自己對volatile變數所作的修改; · 要求volatile修飾的變數不會被指令重排序優化,保證程式碼的執行順序和程式順序相同。 4 Java記憶體模型對long和double型變數的特殊規則 ---long和double的非原子性協定:允許虛擬機器將沒有被volatile修飾的64位資料的讀寫操作劃分為兩次32位的操作來進行。 5 原子性、可見性、有序性
(1)原子性
---8種操作:read、load、assign、use、store、write、lock、unlock。
(2)可見性
---Java語言中,volatile、synchronized、final關鍵字能實現可見性,如下:
· volatile關鍵字:略。
· synchronized關鍵字:由“對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中”這條規則獲得。
· final關鍵字:被final修飾的欄位在構造器中一旦初始化完成,並且構造器沒有把“this”引用傳遞出去,那在其他執行緒中就能看見final欄位的值。
(3)有序性
---如果在本執行緒內觀察,所有的操作都是有序的;如果在一個執行緒中觀察另一個執行緒,所有的操作都是無序的。
---Java語言提供了volatile和synchronized關鍵字來保證執行緒之間操作的有序性,如下:
· volatile關鍵字:本身包含了禁止指令重排序的語義;
· synchronize關鍵字:由“一個變數在同一個時刻只允許一條執行緒對其進行lock操作”這條規則獲得。
6 先行發生原則
---是判斷資料是否存在競爭、執行緒是否安全的的主要依據。
---Java記憶體模型下一些“天然的”先行發生關係:
· 程式次序規則:在一個執行緒內,按照程式控制流順序,控制流前面的操作先行發生於控制流後面的操作。
· 管程鎖定規則:一個unlock操作先行發生於後面對同一個鎖的lock操作。
· volatile變數規則:對一個volatile變數的寫操作先行發生於後面對這個變數的讀操作。
· 執行緒啟動規則:Thread物件的start()方法先行發生於此執行緒的每一個動作。
· 執行緒終止規則:執行緒中的所有操作先行發生於對此執行緒的終止檢測。
· 執行緒中斷規則:對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生。
· 物件終結規則:一個物件的初始化完成先行發生於它的finalize()方法的開始.
· 傳遞性:如果操作A先行發生於操作B,操作B先行發生於操作C,那麼操作A先行發生於操作C。
---先行發生原則和時間先後順序之間基本沒有太大的關係。
7 執行緒的實現
(1)使用核心執行緒實現
---核心執行緒(KLT):由作業系統核心支援的執行緒。由核心來完成執行緒切換,核心通過操縱排程器對執行緒進行排程。
---輕量級程序(LWP):核心執行緒的一種高階介面。每個輕量級程序都由一個核心執行緒支援,輕量級程序與核心執行緒是1:1的關係。
---輕量級程序優點:每個輕量級程序都是一個獨立的排程單元,即使有一個輕量級程序在系統呼叫中阻塞了,也不會影響整個程序繼續工作。
---輕量級程序缺點:
· 各種執行緒操作如建立、析構和同步,都需要進行系統呼叫,而系統呼叫需要在使用者態和核心態之間來回切換,代價相對較高;
· 需要消耗一定的核心資源(如核心執行緒的棧空間),因此一個系統支援輕量級程序的數量是有限的。
(2)使用使用者執行緒實現
---使用者執行緒:完全建立在使用者空間的執行緒庫上,系統核心不能感知執行緒存在的實現。
---使用者執行緒的建立、同步、銷燬和排程完全在使用者態中完成,不需要核心的幫助。
---程序與使用者執行緒之間是一對多的關係。
---使用使用者執行緒實現的優點:
· 程式實現得當,不需要切換到核心態,操作是快速且低消耗的;
· 可以支援規模較大的執行緒數量。
---使用使用者執行緒實現的缺點:所有的執行緒操作都需要使用者程式自己處理,程式一般都比較複雜。 (3)使用使用者執行緒加輕量級程序混合實現 ---使用者執行緒和輕量級程序的數量比是多對多的關係,即N : M。 ---優點: · 使用者執行緒的建立、切換、析構等操作依然廉價; · 支援大規模的使用者執行緒併發; · 使用核心提供的執行緒排程功能及處理器對映,並且使用者執行緒的系統呼叫通過輕量級程序來完成,大大降低了整個程序被完全阻塞的風險。 (4) Java執行緒的實現 ---對於Sun JDK來說,它的Windows版與Linux版都是使用一對一的執行緒模型實現的,一條Java執行緒就對映到一條輕量級程序之中。 8 Java執行緒排程 ---執行緒排程:系統為執行緒分配處理器使用權的過程。
---主要排程方式:
(1)協同式執行緒排程 ---概念:執行緒的執行時間由執行緒本身來控制,執行緒把自己的工作執行完了之後,要主動通知系統切換到另外一個執行緒上。
---優點:實現簡單·、不存線上程同步問題。
---缺點:執行緒執行時間不可控制,如果一個執行緒出現問題,一直不告訴系統進行系統切換,那麼程式就會一直阻塞在那裡。
(2)搶佔式執行緒排程
---概念:每個執行緒由系統來分配執行時間,執行緒的切換不由執行緒本身來決定。
---優點:執行緒的執行時間可控,而且不會因為一個執行緒出現問題而導致整個程序阻塞。
---Java使用的執行緒排程方式就是搶佔式排程。
---Java語言中,可以通過設定執行緒優先順序的方式來調整各個執行緒的執行時間的多少。
---使用執行緒優先順序並不大靠譜,原因是:
· Java的執行緒是通過對映到系統的原生執行緒上來實現的,執行緒排程最終還是取決於作業系統,而不同平臺上的優先順序分類數量不一致。
· 優先順序還可能會被系統自行改變。
9 執行緒狀態
---Java語言中定義了5中執行緒狀態:
(1)新建:建立後尚未啟動的執行緒處於這種狀態。
(2)執行:處於此狀態的執行緒可能正在執行,也可能正在等待CPU為它分配執行時間。
(3)無限期等待:處於這種狀態的執行緒不會被CPU分配執行時間,它們需要被其它執行緒顯式喚醒。
---三種讓執行緒陷入無限期等待的方法:
· 沒有設定Timeout引數的Object.wait()方法;
· 沒有設定Timeout引數的Thread.join()方法;
· LockSupport.park()方法。
(4)限期等待:處於這種狀態的執行緒不會被CPU分配執行時間,也不需要被其它執行緒顯式喚醒,在一定時間後會由系統自動喚醒。 ---五種會讓執行緒進入限期等待的方法: · Thread.sleep()方法; · 設定了Timeout引數的Object.wait()方法; · 設定了Timeout引數的Thread.join()方法; · LockSupport.parkNanos()方法; · LockSupport.partUntil()方法。 (5)阻塞:在程式等待進入同步區域的時候。
(6)結束:已終止執行緒的執行緒狀態。
---轉換關係如下圖:
1 Java記憶體模型
---主要目標:定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體中取出變數這樣的底層細節。
---此處的變數和Java中的變數有所區別,它包括類欄位、例項欄位和構成陣列物件的元素,但不包括區域性變數和方法引數。
---Java記憶體模型規定:
· 所有的變數都儲存在主記憶體中;
·
· 不同的執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數值的傳遞均需要通過主記憶體來完成。
---執行緒、主記憶體、工作記憶體三者的互動關係如下圖:
· lock:作用於主記憶體的變數,把一個變數標識為執行緒獨佔的狀態。
· unlock:作用於主記憶體的變數,把一個處於鎖定狀態的變數釋放出來,釋放後的變數才能被其他執行緒鎖定。
· load:作用於工作記憶體的變數,把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
· use:作用於工作記憶體的變數,把工作記憶體中一個變數的值傳遞給執行引擎。
· assign:作用於工作記憶體的變數,把一個從執行引擎接收到的值賦給工作記憶體的變數。
· store:作用於工作記憶體的變數,把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的write操作使用。
· write:作用於主記憶體的變數,把store操作從工作記憶體中得到的變數的值放入主記憶體的變數中。
---Java記憶體模型還規定了在執行上述8種操作時必須滿足如下規則:
· 不允許read和load、store和write操作之一單獨出現。
· 不允許一個執行緒丟棄它的最近的assign操作,即變數在工作記憶體中改變了之後必須把該變化同步回主記憶體。
· 不允許一個執行緒無原因地(沒有發生過任何地assign操作)把資料從執行緒的工作記憶體同步回主記憶體中。
· 一個新的變數只能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化的變數。
· 一個變數在同一個時刻只允許一條執行緒對其進行lock操作,但lock操作可以被同一條執行緒重複多次執行,多次執行lock後,只有執行相同次數的unlock操作,變數才會被解鎖。
· 如果對一個變數執行lock操作,那將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前,需要重新執行load或assign操作初始化變數的值。
· 如果一個變數事先沒有被lock鎖定,那就不允許對它執行unlock操作,也不允許去unlock一個被其他執行緒鎖定的變數。
· 對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中。
3 volatile變數 ---是Java虛擬機器提供的最輕量級的同步機制; ---volatile變數的特性: · 保證此變數對所有執行緒的可見性。即當一條執行緒修改了這個變數的值,新值對於其他執行緒來說是可以立即得知的; 備註:可見性說明volatile變數在各個執行緒的工作記憶體中不存在一致性問題,但是由於Java裡面的運算並非原子操作,導致volatile變數的運算在併發下一樣是不安全的。 · 禁止指令重排序優化。指令重排序是指CPU採用了允許將多條指令不按程式規定的順序分開發送給各相應電路單元處理,volatile關鍵字保證重排序時不能把後面的指令重排序到記憶體屏障之前的位置。 ---使用volatile必須滿足: · 運算結果並不依賴變數的當前值,或者能夠確保只有單一的執行緒修改變數的值; · 變數不需要與其他的狀態變數共同參與不變約束。 ---效能: · 效能要優於鎖(使用synchronized關鍵字或java.util.concurrent包裡面的鎖); · volatile變數讀操作的效能和普通變數幾乎沒有差別,寫操作可能會慢一些,因為它需要在原生代碼中插入許多記憶體屏障指令來保證處理器不發生亂序執行。 Java記憶體模型對volatile變數定義的特殊規則: · 要求在工作記憶體中,每次使用volatile變數前都必須先從主記憶體重新整理最新的值,用於保證能看見其他執行緒對volatile變數所做的修改後的值; · 要求在工作記憶體中,每次修改volatile變數後都必須立刻同步回主記憶體中,用於保證其他執行緒可以看到自己對volatile變數所作的修改; · 要求volatile修飾的變數不會被指令重排序優化,保證程式碼的執行順序和程式順序相同。 4 Java記憶體模型對long和double型變數的特殊規則 ---long和double的非原子性協定:允許虛擬機器將沒有被volatile修飾的64位資料的讀寫操作劃分為兩次32位的操作來進行。 5 原子性、可見性、有序性
(1)原子性
---8種操作:read、load、assign、use、store、write、lock、unlock。
(2)可見性
---Java語言中,volatile、synchronized、final關鍵字能實現可見性,如下:
· volatile關鍵字:略。
· synchronized關鍵字:由“對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中”這條規則獲得。
· final關鍵字:被final修飾的欄位在構造器中一旦初始化完成,並且構造器沒有把“this”引用傳遞出去,那在其他執行緒中就能看見final欄位的值。
(3)有序性
---如果在本執行緒內觀察,所有的操作都是有序的;如果在一個執行緒中觀察另一個執行緒,所有的操作都是無序的。
---Java語言提供了volatile和synchronized關鍵字來保證執行緒之間操作的有序性,如下:
· volatile關鍵字:本身包含了禁止指令重排序的語義;
· synchronize關鍵字:由“一個變數在同一個時刻只允許一條執行緒對其進行lock操作”這條規則獲得。
6 先行發生原則
---是判斷資料是否存在競爭、執行緒是否安全的的主要依據。
---Java記憶體模型下一些“天然的”先行發生關係:
· 程式次序規則:在一個執行緒內,按照程式控制流順序,控制流前面的操作先行發生於控制流後面的操作。
· 管程鎖定規則:一個unlock操作先行發生於後面對同一個鎖的lock操作。
· volatile變數規則:對一個volatile變數的寫操作先行發生於後面對這個變數的讀操作。
· 執行緒啟動規則:Thread物件的start()方法先行發生於此執行緒的每一個動作。
· 執行緒終止規則:執行緒中的所有操作先行發生於對此執行緒的終止檢測。
· 執行緒中斷規則:對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生。
· 物件終結規則:一個物件的初始化完成先行發生於它的finalize()方法的開始.
· 傳遞性:如果操作A先行發生於操作B,操作B先行發生於操作C,那麼操作A先行發生於操作C。
---先行發生原則和時間先後順序之間基本沒有太大的關係。
7 執行緒的實現
(1)使用核心執行緒實現
---核心執行緒(KLT):由作業系統核心支援的執行緒。由核心來完成執行緒切換,核心通過操縱排程器對執行緒進行排程。
---輕量級程序(LWP):核心執行緒的一種高階介面。每個輕量級程序都由一個核心執行緒支援,輕量級程序與核心執行緒是1:1的關係。
---輕量級程序優點:每個輕量級程序都是一個獨立的排程單元,即使有一個輕量級程序在系統呼叫中阻塞了,也不會影響整個程序繼續工作。
---輕量級程序缺點:
· 各種執行緒操作如建立、析構和同步,都需要進行系統呼叫,而系統呼叫需要在使用者態和核心態之間來回切換,代價相對較高;
· 需要消耗一定的核心資源(如核心執行緒的棧空間),因此一個系統支援輕量級程序的數量是有限的。
(2)使用使用者執行緒實現
---使用者執行緒:完全建立在使用者空間的執行緒庫上,系統核心不能感知執行緒存在的實現。
---使用者執行緒的建立、同步、銷燬和排程完全在使用者態中完成,不需要核心的幫助。
---程序與使用者執行緒之間是一對多的關係。
---使用使用者執行緒實現的優點:
· 程式實現得當,不需要切換到核心態,操作是快速且低消耗的;
· 可以支援規模較大的執行緒數量。
---使用使用者執行緒實現的缺點:所有的執行緒操作都需要使用者程式自己處理,程式一般都比較複雜。 (3)使用使用者執行緒加輕量級程序混合實現 ---使用者執行緒和輕量級程序的數量比是多對多的關係,即N : M。 ---優點: · 使用者執行緒的建立、切換、析構等操作依然廉價; · 支援大規模的使用者執行緒併發; · 使用核心提供的執行緒排程功能及處理器對映,並且使用者執行緒的系統呼叫通過輕量級程序來完成,大大降低了整個程序被完全阻塞的風險。 (4) Java執行緒的實現 ---對於Sun JDK來說,它的Windows版與Linux版都是使用一對一的執行緒模型實現的,一條Java執行緒就對映到一條輕量級程序之中。 8 Java執行緒排程 ---執行緒排程:系統為執行緒分配處理器使用權的過程。
---主要排程方式:
(1)協同式執行緒排程 ---概念:執行緒的執行時間由執行緒本身來控制,執行緒把自己的工作執行完了之後,要主動通知系統切換到另外一個執行緒上。
---優點:實現簡單·、不存線上程同步問題。
---缺點:執行緒執行時間不可控制,如果一個執行緒出現問題,一直不告訴系統進行系統切換,那麼程式就會一直阻塞在那裡。
(2)搶佔式執行緒排程
---概念:每個執行緒由系統來分配執行時間,執行緒的切換不由執行緒本身來決定。
---優點:執行緒的執行時間可控,而且不會因為一個執行緒出現問題而導致整個程序阻塞。
---Java使用的執行緒排程方式就是搶佔式排程。
---Java語言中,可以通過設定執行緒優先順序的方式來調整各個執行緒的執行時間的多少。
---使用執行緒優先順序並不大靠譜,原因是:
· Java的執行緒是通過對映到系統的原生執行緒上來實現的,執行緒排程最終還是取決於作業系統,而不同平臺上的優先順序分類數量不一致。
· 優先順序還可能會被系統自行改變。
9 執行緒狀態
---Java語言中定義了5中執行緒狀態:
(1)新建:建立後尚未啟動的執行緒處於這種狀態。
(2)執行:處於此狀態的執行緒可能正在執行,也可能正在等待CPU為它分配執行時間。
(3)無限期等待:處於這種狀態的執行緒不會被CPU分配執行時間,它們需要被其它執行緒顯式喚醒。
---三種讓執行緒陷入無限期等待的方法:
· 沒有設定Timeout引數的Object.wait()方法;
· 沒有設定Timeout引數的Thread.join()方法;
· LockSupport.park()方法。
(4)限期等待:處於這種狀態的執行緒不會被CPU分配執行時間,也不需要被其它執行緒顯式喚醒,在一定時間後會由系統自動喚醒。 ---五種會讓執行緒進入限期等待的方法: · Thread.sleep()方法; · 設定了Timeout引數的Object.wait()方法; · 設定了Timeout引數的Thread.join()方法; · LockSupport.parkNanos()方法; · LockSupport.partUntil()方法。 (5)阻塞:在程式等待進入同步區域的時候。
(6)結束:已終止執行緒的執行緒狀態。
---轉換關係如下圖: