多執行緒筆記(一)
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在讀取資料的時候,就會從主記憶體去加載最新的資料
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/469599.html
標籤:其他
上一篇:pandas子集選取的三種方法:[]、.loc[]、.iloc[]
下一篇:epoll 函式決議
