- 背景關系切換
- 如何減少背景關系切換
- CAS演算法
- ABA問題
- Java并發機制底層實作
- Volatile關鍵字
- 執行緒可見性
- 防止指令重排序
- volatie的實作
- 記憶體屏障
- 快取行自動填充
- synchronized關鍵字
- synchronized底層實作
- java代碼層級上
- 位元組碼層級上
- Java物件記憶體布局
- 物件頭
- 鎖狀態
- 無鎖
- 偏向鎖
- 輕量級鎖
- 重量級鎖
- 三種鎖的比較
背景關系切換
多執行緒是通過CPU給每個執行緒分配CPU時間片來實作的,所以本質上CPU只能執行一個執行緒而不能執行多個,多執行緒其實就是CPU在不斷地切換執行緒去執行,讓外界感覺像是多個在同時運行
多執行緒的情況下,CPU需要記錄當前任務的狀態才能切換到下一個任務,當再次切換到這個任務的時候,可以從記錄的狀態開始執行,加載進上次執行該任務的狀態,從任務被切換再到被加載進來的程序,實際上就是一次背景關系切換
如何減少背景關系切換
-
少使用鎖,使用鎖會增加切換的次數,從而引起更多背景關系的切換
-
使用CAS演算法
-
使用協程
-
減少執行緒使用,執行緒越多,切換的次數越多,背景關系切換越多
CAS演算法
CAS是Compare And Swap的縮寫,也就是比較和交換,當一個執行緒去修改一個值時,要先比對操作的值是否等于當前的值,如果不等于,代表有其他執行緒修改了,那么就要去進行重新獲取和計算
ABA問題
CAS演算法會有一個問題就是ABA問題,如果操作的值的確是等于當前的值,但當前的值是經過多個執行緒修改后又變回原來的,也就是仍然是被其他執行緒修改過
解決辦法就是,在操作的值加上一個版本號,比較的時候同時也要比較這個版本號,當發生修改的時候,這個版本號要進行變化,比如說自增
Java并發機制底層實作
Volatile關鍵字
Volatile關鍵字有兩個作用
- 執行緒可見
- 防止指令重排序
執行緒可見性
執行緒會將操作的變數都做一份復制,保存到本地記憶體中去,然后根據這份復制去進行操作的
多執行緒出現的問題在于各個執行緒之間不清楚操作的變數是否發生修改,因為都是根據據本地記憶體的副本去進行的,所以會發生,而volatie關鍵字實作了執行緒之間對變數的通信,讓執行緒之間可以看到指定變數的情況
防止指令重排序
Java程式在new一個實體的時候(注意不是類加載的程序),步驟如下
- 在堆中劃分記憶體
- 給物件屬性加上默認值
- 給物件屬性賦上初始值
- 讓堆疊中的變數指向堆中為物件劃分的記憶體,也就是參考賦值
前面兩條是沒有問題的,但后面兩條是可能會發生重排序的
當給物件屬性賦上初始值的時候,如果這個操作耗時比較舊,CPU會先去執行后面的參考賦值的操作,這就是發生了指令重排序
指令重排序對于單執行緒來說是沒有問題的,但對于多執行緒來說就會產生問題
這會導致的問題就是,當一個執行緒去實體化一個變數,此時發生了重排序,還沒有初始值就有參考了(也就是不等于null),那么此時另一個執行緒去獲取這個變數的時候,使用null去判斷這個變數是否創建好的時候,就會出現問題(未初始化完成就可以進行獲取)
volatile關鍵字可以防止指令發生重排序,也就一定要賦上初始值,才可以參考賦值,這樣就解決了上面的問題了
volatie的實作
volatie的實作,其實本質上是一潭訓編的lock指令
這個lock指令有兩步
- 將當前處理器快取行的資料寫回到系統記憶體中
- 這個寫回記憶體的操作會使在其他CPU里快取了該記憶體地址的資料無效
我們先來談談這個系統快取和處理器的快取行
處理器,也就是CPU,也可以理解成當前執行緒,他是不會直接和記憶體(主執行緒的變數都放在記憶體中)進行通信的,而是會先將系統記憶體的資料讀取到內部快取后再進行操作,操作的也是內部快取
如果對volatie修飾的變數進行修改,那么第一步就是將這個變數所在快取行的資料寫回到系統記憶體,也就是修改,第二步就是告知其他CPU里快取了該記憶體地址的資料無效,需要重新去獲取
第二步的底層實作其實就是快取一致性協議,每個處理器都會通過嗅探總線上傳播的資料來檢查自己快取的值是不是過期了,當處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器的快取行設定成無效狀態,當處理器對這個資料進行修改操作的時候,需要重新從系統記憶體中把資料讀取到處理器快取中去
記憶體屏障
同時,volatile還要去實作防止重排序,而防止重排序的底層實作是記憶體屏障
在指令之間加上記憶體屏障,那么上下兩條指令是不可以發生重排序的
現在我們來看看其位元組碼上的實作
也不知道為什么,window的javap -v命令無法顯示變數的位元組碼情況
其實volatile的的位元組碼底層實作是加了一個acc_volatile修飾的,詳細的執行程序就是上面所述
快取行自動填充
上面提到過,volatile的實作是針對快取行來進行操作的,而快取行一般是64位元組的,而往往一個變數可能不夠8位元組,那么就會導致一些不希望通知的資料也通知給了其他執行緒,這個資料也會被設定成無效狀態(無效狀態是整個快取行無效),那么這個無關變數的使用效率就會降低,所以,我們需要快取行填充,來讓一個快取行里面只有volatile變數和一些無關變數
synchronized關鍵字
synchronized在jdk1.6之前是一個重量級鎖(通過作業系統的互斥量來實作的)
之后進行了一系列的優化,在有些情況下沒有變得這么重了
前面復習過,synchronized是一個內部鎖,又分為物件鎖和類鎖,也復習過synchronized可以加在方法上,也可以只鎖住方法里面的一段代碼塊
- 對于類中的非靜態方法,如果在方法上加鎖,加的就是物件鎖
- 對于類中的靜態方法,如果在方法上加鎖,加的就是類鎖
- 對于方法塊,加的是指定的鎖,可以指定是類鎖,也可以指定是物件鎖
synchronized底層實作
我們看看synchronized是如何實作的
首先,我們這里要首先認識的是,synchronized的資訊是放在Java物件頭里面的
java代碼層級上
java代碼就是簡單的加上synchronic關鍵字
位元組碼層級上

我這里創建了一個類,類中有兩個加鎖的方法,一個是方法上加鎖,一個是代碼塊加鎖
接下來看看位元組碼會怎樣
通過比較不加鎖的方法,即sayTwo

先看第一個加鎖的方法,即say方法

通過跟不加鎖的方法比較,方法加鎖與方法不加鎖唯一的不同就是在方法修飾上加了synchronized,但這并不是全部的,所以使用javap -v來看一下真實的

可以看到,底層是多了一個ACC_SYNCHRONIZED的修飾
現在看一下代碼塊


太長了,分兩張來截
可以看到里面多了兩個東西,MonitorEnter與MonitorExit
MonitorEnter指令是在編譯后插入到代碼塊的開始位置的,而MonitorExit是插入到方法結束處和例外處的
任何物件都有自己的一個monitor與之關聯,當這個monitor被持有后,這個物件就會處于鎖定狀態
當有執行緒執行到MonitorEnter指令后,會去嘗試獲取這個物件擁有的monitor的所有權,這一步相當于是獲取物件的鎖
JVM還規定每一個MonitorEnter都必須要有一個MonitorExit與之對應匹配
所以,代碼塊在位元組碼層級上實作的方式就是加MoniorEnter與MonitorExit,而方法加鎖在位元組碼層級上的實作也是加了acc_synchronized來標識這個是一個同步方法
Java物件記憶體布局
鎖的資訊是存在哪里的,所以要去看一下Java物件的記憶體布局
Java物件的記憶體布局有三種
- 物件頭
- markword:記錄鎖的資訊
- 型別指標:標記屬于哪一個class,物件是哪種型別
- 陣列長度:如果該物件是一個陣列,這里會記錄陣列長度
- 實體資料:即一些成員變數
- 對齊:java物件大小必須要可以被8整除,所以后面要進行對齊
物件頭
synchronized用的鎖其實就是存在Java物件頭里面的,而且很明確存在于markword里面,而markword這部分占了8個位元組,也就是64位,不過可能會進行壓縮,從而變成4個位元組,所以即可能是,64位也可能是32位,而整個Java物件頭是12個位元組,前面兩個部分都可能會進行壓縮,從8個位元組變成4個位元組
下面來看看markword的結構
| 鎖狀態 | 25bit(物件的hashcode) | 4bit(分代年齡) | 1bit(是否是偏向鎖) | 2bit(標志位) |
|---|---|---|---|---|
鎖狀態
鎖的狀態一共有4種
- 無鎖
- 偏向鎖
- 輕量級鎖
- 重量級鎖
無鎖
無鎖就是沒有加鎖
偏向鎖
當只有一個執行緒時,即不會出現多執行緒競爭狀態下,就是偏向鎖狀態
當一個執行緒訪問加鎖的方法或代碼塊,就會獲得鎖,獲得鎖的程序其實就是在markword里面記錄自己的執行緒ID,那么以后該執行緒在進入和退出代碼塊時就不需要進行CAS自選來加鎖和執行完后進行解鎖,相當于這個鎖只歸該執行緒擁有
下面來看看偏向鎖是怎樣撤銷的
偏向鎖的撤銷必須要等待全域安全點(在這個時間點上是沒有正在執行的位元組碼的),首先會暫停擁有偏向鎖的執行緒,然后去檢查持有偏向鎖的執行緒是否還存活著,如果執行緒已經不處于活動狀態,則將markword設定成無鎖狀態;如果執行緒仍然存活,進行解鎖(即將Markword里面記錄的執行緒ID設為空),之后再恢復執行緒
- 暫停擁有偏向鎖的執行緒
- 檢查偏向鎖的執行緒是否還存活
- 不存活就代表沒有執行緒去搶這個鎖,設定成無鎖狀態
- 存活就代表仍然會有執行緒去搶這個鎖,此時將markword里面記錄執行緒的ID設為空
- 檢查偏向鎖的執行緒是否還存活
- 恢復執行緒
輕量級鎖
偏向鎖是沒有發生多執行緒競爭的,一旦發生了多執行緒競爭,就會變成輕量級鎖
變成輕量級鎖,必須要先撤銷偏向鎖,因為這個鎖已經不再是一個執行緒專用的了(上面已經提到過偏向鎖怎么撤銷)
輕量級鎖的獲取程序其實就是CAS,當存在爭奪鎖的競爭就會變為輕量級鎖
輕量級鎖的加鎖程序
- 執行緒首先會在自己的堆疊幀種創建用于存盤鎖記錄的空間,并將物件頭中的markword復制進來
- 在競爭程序中,執行緒會嘗試使用CAS演算法,去將物件的markword替換為指向自己鎖記錄的指標(即第一步復制進來的鎖記錄)
- 如果成功讓物件的markword替換為指向自己鎖記錄的指標,就代表該執行緒爭搶到這個鎖了,可以執行
- 如果失敗,則代表markword已經被其他執行緒設定了,當前執行緒便使用自旋來獲取鎖,即一段時間就過來看能否進行爭搶
輕量級鎖的解鎖程序
當持有鎖的執行緒完成操作就要進行解鎖,解鎖的步驟也是很簡單
- 將物件的markword從執行自己鎖記錄的指標替換會原來的markword
重量級鎖
當競爭越來越激烈時,輕量級鎖就會升級成重量級鎖,這是由于越來越多的執行緒會進入自旋狀態,自旋也是要消耗CPU的,因為這個執行緒也是在運行著,降低執行效率,所以會去變成重量級鎖,重量級鎖的原理是作業系統的互斥量
當鎖處于這個狀態下,如果一個執行緒搶到了鎖,那么其他執行緒都會被阻塞住,也就是被掛起,當持有鎖的執行緒釋放鎖之后,才會去喚醒其他執行緒,被喚醒的執行緒會開始新一輪的搶鎖,
三種鎖的比較
| 鎖 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 偏向鎖 | 加鎖和解鎖不需要額外的消耗 | 如果,存在鎖競爭,就要進行額外的鎖撤銷,即撤銷偏向鎖 | 只有一個執行緒,不存在競爭 |
| 輕量級鎖 | 需要進行加鎖和解鎖,不過競爭的執行緒不會發生阻塞,提高了程式的回應速度 | 如果一個搶到鎖的執行緒一直不歸還鎖,那么其他執行緒會一直自旋,消耗CPU | 有多個執行緒,其競爭情況不大,并且程式要追求回應時間,執行速度快 |
| 重量級鎖 | 需要進行加鎖和解鎖,會發生阻塞,搶不到鎖的執行緒會被掛起 | 執行緒會發生阻塞,回應時間緩慢 | 多個執行緒競爭十分激烈 |
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/287329.html
標籤:其他
