Java中的CAS和自旋鎖詳解
什么是CAS
CAS算法(Compare And Swap),即比較并替換,是一種實現并發(fā)編程時常用到的算法,Java并發(fā)包中的很多類都使用了CAS算法。
CAS算法有3個基本操作數:
- 內存地址V
- 舊的預期值A
- 要修改的新值B
CAS使用自旋的方式來交換值,操作步驟為:
- 讀取內存地址V的值保存在A中
- 在原子操作中比較內存地址V的值是否與A相同
- 相同時,修改內存地址V的值為B,原子操作成功。
- 不相同時,循環(huán)執(zhí)行第一至第三步(自旋),直到成功。
什么是自旋鎖?
自旋鎖(spinlock):是指當一個線程在獲取鎖的時候,如果鎖已經被其它線程獲取,那么該線程將循環(huán)等待,然后不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會退出循環(huán)。
自旋鎖與互斥鎖比較類似,它們都是為了解決對某項資源的互斥使用。無論是互斥鎖,還是自旋鎖,在任何時刻,最多只能有一個保持者,也就說,在任何時刻最多只能有一個執(zhí)行單元獲得鎖。
對于互斥鎖,會讓沒有得到鎖資源的線程進入BLOCKED狀態(tài),而后在爭奪到鎖資源后恢復為RUNNABLE狀態(tài),這個過程中涉及到操作系統用戶模式和內核模式的轉換,代價比較高。但是自旋鎖不會引起調用者堵塞,如果自旋鎖已經被別的執(zhí)行單元保持,調用者就一直循環(huán)在那里看是否該自旋鎖的保持者已經釋放了鎖。
自旋鎖的實現基礎是CAS算法機制。CAS自旋鎖屬于樂觀鎖,樂觀地認為程序中的并發(fā)情況不那么嚴重,所以讓線程不斷去嘗試更新。
基于CAS實現-原子操作類
先看一個線程不安全的例子:
public class AutomicDemo {
public static int num = 0;
public static void main(String[] args){
for(int i=0; i<5; i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int j=0; j<200; j++){
num++;
}
}
}).start();
}
/* 主控失眠3秒,保證所有線程執(zhí)行完成 */
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("num=" + num);
}
}輸出結果:
num=950
因為自增操作不是原子性,多線程環(huán)境下,訪問共享變量線程不安全。
解決方法,加synchronized同步鎖:
for(int j=0; j<200; j++){
synchronized (AutomicDemo.class){
num++;
}
}輸出結果:
num=1000
線程安全。
synchronized確保了線程安全,但會讓沒有得到鎖資源的線程進入BLOCKED狀態(tài),而后在爭奪到鎖資源后恢復為RUNNABLE狀態(tài),這個過程中涉及到操作系統用戶模式和內核模式的轉換,代價比較高。
盡管Java1.6為Synchronized做了優(yōu)化,增加了從偏向鎖到輕量級鎖再到重量級鎖的過度,但是在最終轉變?yōu)橹亓考夋i之后,性能仍然較低。
于是JDK提供了一系列原子操作類:AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference等,它們都是基于CAS去實現的,下面我們就來詳細看一看原子操作類。
public class AutomicDemo {
public static AtomicInteger num = new AtomicInteger(0);
public static void main(String[] args){
for(int i=0; i<5; i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int j=0; j<200; j++){
num.incrementAndGet();
}
}
}).start();
}
/* 主控失眠3秒,保證所有線程執(zhí)行完成 */
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("num=" + num.get());
}
}輸出:
num=1000
使用AtomicInteger之后,最終的輸出結果同樣可以保證是200。并且在某些情況下,代碼的性能會比Synchronized更好。
原子類操作方法
- public final int get():取得當前值
- public final void set(int newValue):設置當前值
- public final int getAndSet(int newValue):設置新值并返回舊值
- public final boolean compareAndSet(int expect, int update):如果當前值為expect,則設置為update
- public final boolean weakCompareAndSet(int expect, int update):如果當前值為expect,則設置為update,可能失敗,不提供保障
- public final int getAndIncrement():當前值加1,返回舊值
- public final int getAndDecrement():當前值減1,返回舊值
- public final int getAndAdd(int delta):當前值加delta,返回舊值
- public final int incrementAndGet():當前值加1,返回新值
- public final int decrementAndGet():當前值減1,返回新值
- public final int addAndGet(int delta):當前值加delta,返回新值
AtomicInteger底層原理
所有Atomic相關類的實現都是通過CAS(Compare And Swap)去實現的,它是一種樂觀鎖的實現。
CAS實現放在 Unsafe 這個類中的,其內部大部分是native方法。這個類是不允許更改的,而且也不建議開發(fā)者調用,它只是用于JDK內部調用,看名字就知道它是不安全的,因為它是直接操作內存,稍不注意就可能把內存寫崩。
Unsafe類使Java擁有了像C語言的指針一樣操作內存空間的能力。有一下特點:
1、不受jvm管理,也就意味著無法被GC,需要我們手動GC,稍有不慎就會出現內存泄漏。
2、Unsafe的不少方法中必須提供原始地址(內存地址)和被替換對象的地址,偏移量要自己計算,一旦出現問題就是JVM崩潰級別的異常,會導致整個JVM實例崩潰,表現為應用程序直接crash掉。
3、直接操作內存,也意味著其速度更快,在高并發(fā)的條件之下能夠很好地提高效率。
去看AtomicInteger的內部實現可以發(fā)現,全是調用的Unsafe類中的方法:

CAS機制ABA問題
ABA問題:CAS算法通過比較變量值是否相同來修改變量值以保證原子性,但如果一個內存地址的值本身是A,線程1準備修改為C。在這期間,線程2將值修改為B,線程3將值修改為A,線程1獲取內存地址的值還是A,故修改為C成功。但獲取的A已不再是最開始那一個A。這就是經典的ABA問題,A已不再是A。
如何解決ABA問題呢?
兩個方法,1、增加版本號;2、增加時間戳。
- 增加版本號:讓值的修改從A-B-A-C變?yōu)?A-2B-3A-4C;這樣在線程1 中就能判別出1A不是當前內存中的3A,從而不會更新變量為4C。
- 增加時間戳:值被修改時,除了更新數據本身外,還必須更新時間戳。對象值以及時間戳都必須滿足期望,寫入才會成功。JDK提供了一個帶有時間戳的CAS操作類AtomicStampedeReference。
CAS的缺點
Java原子類使用自旋的方式來處理每次比較不相同時后的重試操作,下面來看看AtomicInteger類incrementAndGet方法的代碼:
//AtomicInteger 的incrementAndGet方法,變量U為靜態(tài)常量jdk.internal.misc.Unsafe類型
public final int incrementAndGet() {
//使用getAndAddInt方法,實際操作類似j++
return U.getAndAddInt(this, VALUE, 1) + 1;
}
//jdk.internal.misc.Unsafe類型的getAndAddInt方法
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
//獲取變量o的可見值
v = getIntVolatile(o, offset);
//比較與替換變量o(CAS算法)
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
//jdk.internal.misc.Unsafe類型的weakCompareAndSetInt方法
public final boolean weakCompareAndSetInt(Object o, long offset,
int expected,
int x) {
//執(zhí)行比較與替換
return compareAndSetInt(o, offset, expected, x);
}在Unsafe類的getAndAddInt方法中使用了do…while循環(huán)語句,循環(huán)語句的作用為自旋,Unsafe的weakCompareAndSetInt實現CAS算法。
如果weakCompareAndSetInt一直不成功將會一直自旋下去,這將消耗過多的CPU時間。而且原子類使用CAS算法實現,這導致原子類只能保證一個變量的原子操作,對于需要保證一個具有多個操作的事務將變得無能為力。
總結如下:
- CPU開銷較大
- 在并發(fā)量比較高的情況下,如果許多線程反復嘗試更新某一個變量,卻又一直更新不成功,循環(huán)往復,自旋會給CPU帶來很大的壓力。
- 不能保證代碼塊的原子性
- CAS機制所保證的只是一個變量的原子性操作,而不能保證整個代碼塊的原子性。比如需要保證3個變量共同進行原子性的更新,就不得不使用Synchronized了。
為應對CAS存在缺點,替換方案如下:
- 自旋效率:Java提供了自適應自旋鎖
- 片面性:應對片面性問題Java提供了讀寫鎖
- ABA問題:用AtomicStampedReference/AtomicMarkableReference解決ABA問題
到此這篇關于Java中的CAS和自旋鎖詳解的文章就介紹到這了,更多相關Java的CAS和自旋鎖內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java設計模式之命令模式_動力節(jié)點Java學院整理
命令模式就是對命令的封裝,下文中給大家介紹了命令模式類圖中的基本結構,對java設計模式之命令模式相關知識感興趣的朋友一起看看吧2017-08-08
Springboot整合WebSocket實戰(zhàn)教程
WebSocket使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據,這篇文章主要介紹了Springboot整合WebSocket實戰(zhàn)教程,需要的朋友可以參考下2023-05-05

