多線程筆記(一) 1. sleep()方法和yield()方法 共同點:讓當前線程釋放cpu資源,讓其他線程來運行 不同點:調用sleep()方法後,線程進入到TIMED_WAITING狀態,等待超時後進入RUNNABLE狀態,開始搶占CPU資源。調用yield()方法後,線程進入RUNNABLE狀 ...
多線程筆記(一)
1. sleep()方法和yield()方法
-
共同點:讓當前線程釋放cpu資源,讓其他線程來運行
-
不同點:調用sleep()方法後,線程進入到TIMED_WAITING狀態,等待超時後進入RUNNABLE狀態,開始搶占CPU資源。調用yield()方法後,線程進入RUNNABLE狀態,直接開始搶占CPU資源。
2. 偏向鎖和可重入鎖的區別
-
偏向鎖是指一個線程訪問同步塊的時候,第一次會獲取鎖,在沒有其他線程競爭鎖的情況下再訪問同步塊不需要再獲取鎖,直接訪問同步塊。節省了獲取鎖和釋放鎖的開銷。
-
可重入鎖是指一個線程訪問同步塊1需要鎖A並獲得鎖A,接下來訪問另一個同步塊2也需要鎖A,線上程持有鎖A的情況下訪問同步塊2時不需要再獲取鎖。
3. wait, notify/notifyAll註意事項
- wait, notify/notifyAll 必須放到同步塊或者同步方法裡面去執行
- 註意使用鎖的對象來調用wait, notify/notifyAll
- 其底層使用的是Monitor機制,wait過後線程會進入monitor對象的對應的WaitSet
- 調用wait方法後就會釋放鎖
- 調用notify/notifyAll方法後並不會立即獲得鎖。要等前面的線程釋放鎖之後再去爭搶鎖
4. Java記憶體模型(JMM)
- 所有變數(共用的)都存儲再主記憶體中,每個線程都有自己的工作記憶體,工作記憶體中保存該線程使用到的變數的主記憶體副本拷貝
- 線程對變數的所有操作(讀,寫)都應該再工作記憶體中完成
- 不同線程不能相互訪問工作記憶體,交互數據要通過主記憶體
記憶體之間的交互操作
-
lock: 鎖定,把變數表示為線程獨占,作用於主記憶體變數
-
read: 讀取,把變數值從主記憶體讀取到工作記憶體
-
load: 載入,把read讀取到的值放入工作記憶體的變數副本中
-
use: 使用,把工作記憶體中的一個變數的值傳遞給執行引擎
-
assign: 賦值,把從執行引擎接收到的值賦給工作記憶體裡面的變數
-
store: 存儲,把工作記憶體中的一個變數的值傳遞到主記憶體中
-
write: 寫入,把store進來的數據存放如主記憶體的變數中
-
unlock: 解鎖,把鎖定的變數釋放,別的線程才能使用,作用於主記憶體變數
記憶體間交互操作的規則
- 不運行read和load;store和write操作之一單獨出現,以上兩個操作必須按順序執行,但不保證連續執行,也就是說,read和load;store和write之間是可以插入其他指令的
- 不允許一個線程丟棄它的最近的assign操作,即變數在工作記憶體中改變了之後必須把該變化同步回主記憶體
- 不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作記憶體同步回主記憶體中
- 一個新的共用變數只能從主記憶體中“誕生”,不允許在工作記憶體中直接使用一個未被初始化的變數,也就是對一個變數實施use和store操作之前,必須先執行過了load和assign操作
- 一個共用變數在同一時刻只允許一個線程對其執行lock操作,但lock操作可以被同一個線程重覆執行多次,多次執行lock後,只有執行相同次數的unlock操作,變數才會被解鎖
- 如果對一個共用變數執行lock操作,將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前,需要重新執行load
- 如果一個共用變數沒有被lock操作鎖定,則不允許對它執行unlock操作,也不能unlock一個被其他線程鎖定的共用變數
- 對一個共用變數執行unlock操作之前,必須先把此變數同步回主記憶體(執行store和write操作)
5. 併發編程三大特性
原子性:一個操作要麼全部執行成功,要麼全部執行失敗
可見性:一個線程修改了共用變數之後,其他線程能夠立刻看到這個修改
有序性:程式執行的順序是按照代碼的邏輯先後循序來執行的
6. 重排序
編譯器或處理器為了優化程式的執行性能,對指令執行的順序重新排列
目的:儘可能減少寄存器的讀取和存儲次數,復用寄存器存儲的數據
分類
- 編譯器重排序:編譯器再不改變程式在單線程環境下運行的語義的前提下,重新安排語句的執行順序
- 指令重排序:處理器將多條指令並行執行,如果不存在數據依賴,處理器可以改變語句對應的指令的執行順序
- 記憶體系統重排序:處理器使用緩存和讀寫緩衝區,使得數據的載入存儲操作看上去是亂序執行的
數據依賴
如果兩個操作訪問同一個共用變數,而且兩個操作裡面有一個為寫操作,那麼這兩個操作直接就存在數據依賴性。
具有數據依賴性的指令是不會被重排的
數據依賴的分類:
- 讀後寫:讀一個變數過後,再寫這個變數
- 寫後寫:寫一個變數過後,再寫這個變數
- 寫後讀:寫一個變數過後,再讀這個變數
as-if-serial語義
不管有沒有重排序,也不關心如何進行重排序,單線程環境下,程式的執行結果不會被改變。
7. happens-before
-
如果一個操作happens-before另一個操作,那麼第一個操作的執行結果將對第二個操作可見(保障可見性)
且第一個操作的執行順序排在第二個操作之前(JMM對程式員做出的一個邏輯保障,並不是代碼指令真正的執行保障)
-
即使兩個操作之間存在happens-before關係,並不意味著Java平臺的實現必須要按照happens-before關係指定的順序來執行
第一條是JMM對程式員做出的邏輯保障
第二條是JMM對編譯器,處理器進行重排序的約束原則:只要不改變程式的執行結果(不管是單線程還是多線程),愛怎麼排怎麼排
happens-before規則
-
程式順序規則:一個線程中的每個操作 happens-before 該線程中的任意後續操作
-
監視器鎖規則:對一個鎖的解鎖操作 happens-before 隨後對這個鎖的加鎖(就是先釋放鎖,後加鎖)
-
volatile變數規則:對一個volatile修飾的欄位進行的寫操作 happens-before 任意後續對這個欄位進行的讀操作
-
傳遞性:如果A happens-before B,且B happens-before C,那麼A happens-before C
-
start規則:如果線程A里執行操作ThreadB.start()(啟動線程B),那麼A線程的ThreadB.start()操作 happens-before 於線程B中的任意操作
-
join規則:如果線程A執行操作ThreadB.join()併成功返回,那麼線程B中的任意操作happens-before於線程A從ThreadB.join()操作成功返回。
8. 記憶體屏障
記憶體屏障是一種屏障指令,它使得處理器或編譯器對屏障指令的前面和後面所發出的記憶體操作,執行一個排序的約束。也叫記憶體柵欄或柵欄指令
作用
- 阻止屏障兩邊的指令重排序
- 寫數據的時候加了屏障的話,強制把寫緩衝區的數據刷回到主記憶體中
- 讀數據的時候加了屏障的話,讓工作記憶體或CPU高速緩存當中緩存的數據失效,重新到主記憶體中獲取新的數據
分類
讀屏障:Load Barrier: 在讀指令之前插入讀屏障,讓工作記憶體或CPU高速緩存當中緩存的數據失效,重新到主記憶體中獲取新的數據
寫屏障:Store Barrier: 在寫指令之後插入寫屏障,強制把緩衝區的數據刷回到主記憶體中
9. 重排序與記憶體屏障
JVM本身為了保證可見性:
對於編譯器的重排序,JMM會根據重排序的規則,禁止特定類型的編譯器重排序
對於處理器的重排序,Java編譯器在生成指令序列的適當位置,插入記憶體屏障指令,來禁止特定類型的處理器排序
JMM的記憶體屏障
- LoadLoad Barriers:
示例:Load1; LoadLoad; Load2
禁止重排序,訪問Load2的讀取操作一定不會重排到Load1之前。由於在讀指令之前插入讀屏障,所有會保證Load2在讀取的時候,自己緩存內相應數據失效,Load2會重新到主記憶體中獲取最新的數據
- LoadStore Barriers:
示例:Load1; LoadStore; Store2
禁止重排序,一定是Load1讀取數據完成後,才能讓Store2寫操作的數據寫入到主記憶體
- StoreStore Barries:
示例:Store1; StoreStore; Store2
禁止重排序,一定是Store1的數據寫入主記憶體後,才能讓Store2寫操作的數據寫入主記憶體。由於在寫指令之後插入寫屏障,所以會保證Store1寫出的數據強制刷回到主記憶體中
- StoreLoad Barries:
示例:Store1; StoreLoad; Load2
禁止重排序,一定是Store1的數據寫入主記憶體後,才能讓Load2讀取數據。由於在寫指令之後插入寫屏障,所以會保證Store1寫出的數據強制刷回到主記憶體中。由於在讀指令之前插入讀屏障,所有會保證Load2在讀取的時候,自己緩存內相應數據失效,Load2會重新到主記憶體中獲取最新的數據。
為什麼說StoreLoad Barries是最重(和記憶體交互次數多,交互延遲較大)的?
因為其既要保證讀屏障也要保證寫屏障
擴展
這些屏障指令並不是處理器真實的執行指令,它們知識JMM定義出來的,跨平臺的指令。因為不同硬體實現記憶體屏障的方式並不相同,JMM為了屏蔽這種底層硬體平臺的不同,抽象出了這些記憶體屏障指令,在運行的時候,由JVM來為不同的平臺生成相應的機器碼。這些記憶體屏障指令,在不同的硬體平臺上,可能會做一些優化,從而只支持部分的JMM的記憶體屏障指令
10. Volatile關鍵字
volatile修飾的變數有如下特點:
- 保證可見性
- 不保證原子性
- 禁止指令重排
-
對一個volatile修飾的變數進行讀操作的話,總是能夠讀到這個變數的最新的值,也就是這個變數最後被修改的值
-
一個線程修改了volatile修飾的變數的值的時候,那麼這個變數的新的值,會立即刷新回到主記憶體中
-
一個線程去讀取volatile修飾的變數的時候,該變數在工作記憶體中的數據無效,需要重新到主記憶體去讀取最新的數據
volatile記憶體語義
volatile寫的記憶體語義:寫一個volatile變數時,JMM會把該線程對應的工作記憶體中的共用變數的值刷新到主記憶體中
volatile讀的記憶體語義:讀一個volatile變數時,JMM會把線程對應的工作記憶體中的共用變數數據設置為無效的,然後從主記憶體中去讀共用變數最新的數據
volatile記憶體語義的實現
-
位元組碼層面:
它影響的是Class內的Field的 falgs ,添加了一個ACC_VOLATILE。JVM在把位元組碼生成為機器碼的時候,發現操作的是volatile變數的話,就回根據JMM的要求,在相應的位置去插入記憶體屏障
-
JMM層面:
第一個操作 | 第二個操作(普通讀寫) | 第二個操作(volatile讀) | 第二個操作(volatile寫) |
---|---|---|---|
普通讀寫 | 不允許重排序 | ||
volatile讀 | 不允許重排序 | 不允許重排序 | 不允許重排序 |
volatile寫 | 不允許重排序 | 不允許重排序 |
volatile寫之前的操作都禁止重排序到volatile之後
volatile讀之後的操作都禁止重排序到volatile之前
volatile寫之後的volatile讀,禁止重排序
為了實現volatile記憶體語義,按如下方式插入記憶體屏障
(1)在每個volatile寫操作的前面插入一個StoreStore屏障
(2)在每個volatile寫操作的後面插入一個StoreLoad屏障
(3)在每個volatile讀操作的後面插入一個LoadLoad屏障
(4)在每個volatile讀操作的後面插入一個LoadStore屏障
- 處理器層面:
CPU執行機器碼指令的時候,是使用 lock 首碼指令來實現volatile的功能的
lock指令相當於記憶體屏障,功能也類似於記憶體屏障的功能
(1)首先對匯流排/緩存加鎖,然後去執行後面的指令,最後釋放鎖,同時把CPU高速緩存的數據刷新回到主記憶體
(2)在lock鎖住匯流排/緩存的時候,其他CPU的讀寫請求就會被阻塞,直到鎖被釋放。lock過後的寫操作,讓會其他CPU的高速緩存中相應的數據失效,這樣後續這些CPU在讀取數據的時候,就會從主記憶體去載入最新的數據