目錄
- 前言
- 1.鎖策略
- 1.1 樂觀鎖和悲觀鎖
- 1.2 重量級鎖和輕量級鎖
- 1.3 掛起等待鎖和自旋鎖
- 1.4 公平鎖和非公平鎖
- 1.5 可重入鎖和不可重入鎖
- 1.6 讀寫鎖
- 2.CAS
- 2.1 CAS的應用
- 2.2 CAS的ABA問題
- 3.synchronized優化
- 3.1鎖升級
- 3.2鎖消除
- 3.3鎖粗化
- 總結
前言
本篇文章主要介紹多線程中鎖策略、CAS和synchronized優化,這三個都是多線程中比較重要的部分。
1.鎖策略
在多線程編程中,鎖是保證線程安全的重要手段。不同的鎖策略適用于不同的場景,了解這些策略有助于我們編寫高效、安全的并發程序。
1.1 樂觀鎖和悲觀鎖
在使用鎖的時候,預測這個鎖遇到沖突的概率
如果預測鎖沖突概率比較高,就稱為“悲觀鎖”,在悲觀鎖中,更傾向于阻塞操作,在訪問數據前就進行加鎖。
如果預測鎖沖突概率比較低,就稱為“樂觀鎖”,在樂觀鎖中,就會嘗試其他方式代替阻塞(例如忙等)。
1.2 重量級鎖和輕量級鎖
重量級鎖表示加鎖的開銷比較大,等待鎖的線程,等待時間可能會比較長。
輕量級鎖表示加鎖的開銷比較小,等待鎖的線程會相對的比較短。
1.3 掛起等待鎖和自旋鎖
這里的兩個鎖是實現層面的。
掛起等待鎖是悲觀鎖/重量級鎖的典型實現,遇到鎖沖突,就會讓線程掛起等待(調度出CPU,等待被CPU喚醒)
自旋鎖則是樂觀鎖/輕量級鎖的典型實現,遇到鎖沖突,不會放棄CPU,而是通過忙等的方式,再次嘗試獲取鎖。
1.4 公平鎖和非公平鎖
公平鎖遵循“先來后到”,按照線程嘗試抓取鎖的順序來進行獲取。
非公平鎖不遵循“先來后到”,而是“概率均等”,每一個嘗試抓取鎖的線程在線程解鎖后都有可能抓取到。
1.5 可重入鎖和不可重入鎖
一個線程針對一個鎖連續加鎖兩次,不觸發死鎖,則是可重入鎖,反之則是不可重入鎖。
1.6 讀寫鎖
讀寫鎖會把加鎖操作分成兩種:讀鎖和寫鎖,在很多場景下,數據讀的次數比寫的次數要頻繁很多,所以把讀寫鎖分開,在讀鎖和讀鎖之間不會產生互斥,讀鎖和寫鎖,寫鎖和寫鎖之間才會產生互斥。這樣降低了很多鎖沖突帶來的性能損失。
2.CAS
CAS全稱compare and swap(比較和交換)
我們假設內存中原數據是V,舊的預期值是A,需要修改的新值是B,我們首先比較V和A是否相等,如果相等,將B寫入V,然后返回一個布爾值。
這里比較特殊的是,這個邏輯是通過一個CPU指令來完成的,
A和B是存儲在兩個寄存器當中,假設寄存器分別為寄存器1,寄存器2,將原數據與寄存器1的值比較,如果相等,就把原數據的值賦值給寄存器2。也就是說,這個操作是原子的,意味著線程安全,不需要加鎖。
2.1 CAS的應用
- cas實現原子類
在標準庫中java.util.concurrent.atomic包里面的類都是基于這種方式來實現的,下面是這個包中的各種類:
這里典型的一個類就是AtomicInteger,其中getAndIncrement就相當于i++操作,下面看一個例子:
private static AtomicInteger count = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count.getAndIncrement(); // 使用原子操作來增加計數}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count.getAndIncrement(); // 使用原子操作來增加計數}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
這里使用了AtomicInteger類來實現多線程實現同時自增一個變量,可以看到不進行加鎖的情況下,結果也正確:
- CAS實現自旋鎖
使用CAS可以實現自旋鎖,下面看偽代碼:
public class SpinLock {private Thread owner = null;public void lock(){// 通過 CAS 看當前鎖是否被某個線程持有.// 如果這個鎖已經被別的線程持有, 那么就?旋等待.// 如果這個鎖沒有被別的線程持有, 那么就把 owner 設為當前嘗試加鎖的線程.while(!CAS(this.owner, null, Thread.currentThread())){}}public void unlock (){this.owner = null;}
}
循環里判斷鎖是否被占用,如果被占用,就會一直循環,直到不被占用,解鎖狀態。
2.2 CAS的ABA問題
假設有兩個線程T1和T2:
線程T1讀取共享變量的值為A。
然后線程T2將共享變量的值從A修改為B,再修改回A。
最后線程T1再次讀取共享變量的值為A,由于值與最初讀取的A相同,因此CAS操作成功,但實際上共享變量的狀態已經被T2修改過。
這樣T1就無法區分這個變量是否經歷了一系列修改還是始終是A
解決方案:
可以給要修改的值引入版本號,比較當前值和舊值時,也要比較版本號是否符合預期:如果當前版本號和讀到的版本號相同, 則修改數據, 并把版本號 + 1;如果當前版本號?于讀到的版本號. 就操作失敗(認為數據已經被修改過了)。
3.synchronized優化
3.1鎖升級
synchronized具有自適應的特性,會根據情況進行鎖升級,JVM將synchronized分為無鎖、偏向鎖、輕量級鎖、重量級鎖狀態,會依次進行升級。
輕量級鎖和重量級鎖前面已經簡要介紹,這里介紹一下偏向鎖。
偏向鎖并沒有給線程真正加鎖,只是記錄這個鎖屬于哪個線程,如果沒有線程進行競爭這個鎖,偏向鎖狀態就保持,直到最終解鎖;如果遇到其他線程來競爭這個鎖,就在其他線程獲取鎖之前,搶先獲取到這個鎖,也可以讓其他線程阻塞,保證線程安全。
3.2鎖消除
有些代碼中寫了加鎖,但是如果JVM執行時發現不需要加鎖,就會自動把鎖去掉。比如StringBuffer帶有synchronized鎖,如果在單線程情況下使用,JVM和編譯器則會進行判斷,將鎖”消除“。
3.3鎖粗化
這里涉及的是鎖的粒度
加鎖和范圍中代碼越多,鎖的粒度越粗。
如果有一系列獨立的加鎖、解鎖操作,JVM可以回將他們合并成一個更大的鎖范圍,只加鎖解鎖一次,這樣減少了系統的調用開銷,同時也減少了鎖競爭。
總結
本篇文章介紹了鎖策略,CAS以及synchronized的優化,在后面會對synchronized進行總結。