多線程-進階
1. 鎖的策略
1.1 樂觀鎖&悲觀鎖
-
樂觀鎖
-
預計在線程中數據大概率不會被其他線程拿去修改
-
對于加鎖所作的準備較少。只有當修改的操作真正發生了,才會進行加鎖操作
所以樂觀鎖適用于多讀少寫的情況,可以降低加鎖頻率,提升效率。
-
-
悲觀鎖
- 預計在線程中數據大概率會被其他線程拿走做修改操作
- 加鎖前的準備工作比較多
所以悲觀鎖適用于對于線程安全要求高的場景。
1.2 輕量級鎖&重量級鎖
-
輕量級鎖
- 對應于樂觀鎖
- 加鎖前的操作占用的資源少,造成的阻塞情況少
- 較少進行內核態和用戶態的切換
- 較少進程之間的調度
-
重量級鎖
- 對應于悲觀鎖
- 加鎖前的準備工作多,容易造成線程阻塞
- 大量內核態和用戶態的切換
- 易引發進程之間的調度
樂觀和悲觀是對于未加鎖結果的一種猜想
重量輕量是對于加鎖后資源消耗的一種評價
從這個角度說,這兩組概念都是在描述加鎖的工作對于資源的消耗。樂觀就是消耗的資源較少,悲觀反之。
1.3 自旋鎖&掛起等待鎖
-
自旋鎖
-
重復、快速地進行鎖的獲取(稱為自旋“)
-
會增大cpu的消耗,可能會造成“忙等”
-
適用于樂觀、輕量的策略(雖然一直不停地在獲取鎖,但是過不了多久就能夠真正獲取到鎖)
-
偽代碼:
while(搶鎖失敗) {加鎖... }
優點:
-
在鎖競爭平緩的情況下能夠降低資源消耗,加快運行速度,避免線程之間因為簡單的任務而阻塞。
-
其他線程一旦把鎖釋放,就會第一時間拿到鎖
缺點:
- 如果遇到鎖競爭激烈的情況,會有其他線程競爭不到鎖的情況。
- 競爭不到鎖,但是還會一直進行獲取鎖,造成cpu的資源浪費
-
-
掛起等待鎖
-
遇到鎖競爭的情況就掛起等待
-
適用于鎖競爭激烈的情況
-
與悲觀、重量相對應
-
偽代碼:
while(搶鎖失敗) {wait(); }
優點:
避免了cpu資源浪費
缺點:
不能第一時間搶到鎖,什么時候能加鎖,由系統決定 -
1.4 普通互斥鎖&讀寫鎖
-
普通互斥鎖
- 類似于synchronized,在一個線程對于同一個對象進行加鎖的時候另一個線程不能對于這個對象的鎖進行獲取
-
讀寫鎖
-
讀鎖
- 給讀操作加鎖,讀的過程中其他線程只能再讀,不能寫
讀的操作并沒有線程安全問題,但是只局限于讀,如果讀的過程中,把正在讀取的值拿走進行修改,那么就會產生讀到“臟值”的情況。
-
寫鎖
- 給寫操作加鎖,在一個線程寫的時候其他線程不能讀,也不能寫
寫就是修改,修改就會有線程安全問題,如果在修改的過程中讀就可能會讀到“臟值”,如果在修改的過程中繼續修改就可能會引起數據的混亂。
-
1.5 公平鎖&非公平鎖
-
公平鎖
- 基于前人發明的、前人規定的規則,“先來后到”就是公平
- “先來后到”:一個線程擁有鎖的時間越長,那就越應該下一個獲取到鎖
- 有效避免了線程餓死
-
非公平鎖
- 在釋放鎖之后,隨機等概率競爭鎖的情況
- mutex鎖就是非公平鎖,synchronized是封裝mutex的鎖,故而也是非公平鎖
這里的“公平”和“非公平”只是基于前人發明的角度上,其實,在“等概率競爭鎖”的情況下也是一種“公平“。
1.6 可重入鎖&不可重入鎖
-
可重入鎖
-
可以重復加鎖
-
synchronized就是可重入鎖
-
偽代碼:
lock();// 第一次加鎖之后繼續加鎖 lock();// 第二次加鎖>
-
-
不可重入鎖
- 不可重復加鎖
- c++中的mutex就是不可重入鎖
- 不可重入鎖在進行重復加鎖的時候會出現死鎖現象
結論:
-
樂觀鎖-輕量級鎖-自旋鎖都是對應的
-
悲觀鎖-重量級鎖-掛起等待鎖是對應的
-
樂觀:認為自己的家不會被偷,那就在家被偷或者被賊盯上的時候再去給門上鎖
悲觀:認為自己的家已經被賊盯上了,一直上著鎖
-
輕量級鎖:只給家門上一個容易打開的鎖,開鎖的時候消耗自己的時間精力也會較少,樂觀地認為賊不會偷有鎖的家
重量級鎖:給家門上一個不容易打開的防盜鎖,,開鎖的時候消耗自己的時間精力會增加,悲觀地認為一直有賊盯上我的房子
-
自旋鎖: 樂觀地認為沒有多少賊盯上了自己的家,所以每天都去看看自己的家有沒有被偷,優點是家一被偷就能夠知道,就能立馬上鎖,缺點是費時費力
掛起等待鎖:悲觀地認為有很多賊已經盯上了自己的家,所以上一把不容易打開的鎖,當有人敲門的時候再去檢查是誰來,如果是賊那就不開門,讓鎖掛起等待,自己繼續在家里玩游戲。好處是減少了自己的任務量,可以有效防止多個賊同時頂上自己家的情況,缺點是這把鎖自己也不容易打開
-
普通互斥鎖:
2. CAS
樂觀鎖的常用實現算法是CAS算法,全稱是CompareAndSwap(比較和交換),這個也是cpu中存在的一條cas指令。
偽代碼:
boolen cas(address, expectedValue, swapValue) {if (&address == expectedValue) {&address = swapValue;return true;}return false;
}
address表示內存,expectedValue和swapValue是兩個寄存器。
如果內存中的值與交換前所期望的值相等,那就與準備交換的值進行交換(說是交換,其實就是賦值)。
expectedValue就是內存中值的”舊值“,通過與這個”舊值“進行比較,就能夠發現此值在修改前是否進行了改動,防止讀到”臟值“。
3. synchronized 原理
3.1 鎖的升級
synchronized鎖是一把自適應鎖,當一個線程執行到synchronized的時候,如果這個線程還未加上鎖,那么synchronized就會經理以下過程:
-
偏向鎖階段
核心思想就是”懶漢模式”,非必要不加鎖(升級成輕量級鎖就是必要的時候)。
- 在這個階段并不會真正地加上鎖,但是會對于有加鎖可能性的對象在對象頭進行標記,標記這個對象屬于哪個線程
- 如果后續沒有線程競爭這把鎖,那就不再真正的進行加鎖,就省下了加鎖的消耗
- 如果一旦發生線程安全問題,立馬升級為輕量級鎖
感覺有點像占著茅坑不拉屎,一旦有人來了就蹲下拉屎,沒人來也就占著。(學校的占座不就是這樣嗎🐶)
-
輕量級鎖階段
-
隨著線程之間少量的鎖競爭,偏向鎖狀態被消除(并不是解鎖了),進入輕量級鎖階段,這個階段由自旋鎖進行實現
-
synchronized內部會統計當前這個鎖對象有多少個線程想要競爭,如果數量多,那么還會升級為重量級鎖
因為鎖的競爭大的話,對于自旋鎖來說大量的線程都在自旋,這樣不能提高效率,反而會帶來更多的cpu消耗。
-
-
重量級鎖階段
- 隨著線程之間大量的鎖競爭,輕量級鎖升級為重量級鎖
- 此時不會對于鎖競爭發生自旋,而是進入阻塞等待狀態,就會有大部分的線程讓出cpu
- 當目前占有鎖的線程執行完畢以后就會釋放鎖,剩余線程等概率競爭鎖
3.2 鎖的工作原理
-
synchronized鎖是:
- 對于以下鎖的狀態自適應:
- 樂觀、悲觀鎖
- 自旋、掛起等待鎖
- 輕量級、重量級
- 不是讀寫鎖
- 非公平鎖
- 可重入鎖
- 對于以下鎖的狀態自適應:
-
系統原生mutex鎖是:
- 悲觀鎖
- 重量級鎖
- 掛起等待鎖
- 互斥鎖
- 非公平鎖
- 不可重入鎖
4. 其他的優化操作
4.1 鎖消除
當編譯器發現加鎖的這一部分代碼中,并未涉及變量修改的部分,那么就會自動將鎖去掉。
使用線程安全的StringBuffer舉個例子:
StringBuffer sb = new StringBuffer(); sb.append("a"); sb.append("b"); sb.append("c"); sb.append("d");
這段代碼中雖然每個append 都有加鎖解鎖的操作,但是jvm+編譯器都發現這段代碼并沒有真正的線程安全問題,所以就不會進行加鎖和解鎖,避免了資源的無謂消耗。
4.2 鎖粗化
與鎖粗化關系密切的一個重要概念是鎖的粒度。
鎖的粒度就像吃的牛肉粒一樣,每次加一個鎖就是一個牛肉粒,有兩種策略吃:
- 每次剝開一個牛肉粒就吃一個
- 每次剝開不吃,等到攢成一個大牛肉粒再吃
兩種偽代碼分別對應這兩種情況:
for() {synchronized(locker) {// TODO} }
將鎖粗化:
synchronized(locker) {for() {// TODO} }
4. 死鎖
4.1 死鎖的成因有4個:
-
循環等待
當鎖A喚醒需要鎖B先釋放,鎖B喚醒需要鎖A先釋放,就構成了循環等待。
-
請求和保持
當一個線程獲取新鎖的同時,它會繼續對于當前已有的鎖進行占有。
-
互斥使用
資源被一個線程占有時,其他的線程不能同時使用。
-
不可搶占
資源被一個線程占有時,其他線程不能搶占使用,只能等待當前占有者主動釋放。
其中,3和4是鎖的特性無法改變,但是1和2可以通過代碼結構進行破壞。
比較著名的是“哲學家就餐問題”:
當這5名哲學家都需要吃飯的時候,餐桌上擁有的筷子數量肯定是不夠用的,但是如果給哲學家加上拿筷子的順序,那么就只會有一名哲學家在“阻塞等待”。
比如:規定每名哲學家只能先拿起編號小的筷子,那么就是0號老鐵先吃
1號老鐵進行阻塞等待,等到0號老鐵吃完以后再先拿起編號小的1號筷子+2號筷子吃
2,3,4老鐵同理。
4.2 破壞死鎖
就是調整代碼結構,破除循環等待這個條件。