1.悲觀鎖
示例程式:啟動兩個線程,每個線程中讓靜态變量count循環累加100次。
最終輸出的count結果是什麼呢?一定會是200嗎?
加了同步鎖之後,count自增的操作變成了原子性操作,是以最終的輸出一定是count=200,代碼實作了線程安全。
Synchronized Lock悲觀鎖
為什麼這麼說呢?關鍵在于性能問題。
Synchronized關鍵字會讓沒有得到鎖資源的線程進入
BLOCKED
狀态,而後在争奪到鎖資源後恢複為
RUNNABLE
狀态,這個過程中涉及到作業系統使用者模式和核心模式的轉換,代價比較高。
盡管Java1.6為
Synchronized
做了優化,增加了從偏向鎖到輕量級鎖再到重量級鎖的過度,但是在最終轉變為重量級鎖之後,性能仍然較低。
2.更高效的方案:AtomicXXXX 原子類
所謂原子操作類,指的是
java.util.concurrent.atomic
包下,一系列以
Atomic
開頭的包裝類。例如
AtomicBoolean
,
AtomicInteger
AtomicLong
。它們分别用于
Boolean
Integer
Long
類型的原子性操作。
現在我們嘗試在代碼中引入
AtomicInteger
類:
使用AtomicInteger之後,最終的輸出結果同樣可以保證是200。并且在某些情況下,代碼的性能會比Synchronized更好。
3.什麼是CAS?
CAS是英文單詞Compare And Swap的縮寫,翻譯過來就是比較并替換。
CAS機制當中使用了3個基本操作數:記憶體位址V,舊的預期值A,要修改的新值B。
更新一個變量的時候,隻有當變量的預期值A和記憶體位址V當中的實際值相同時,才會将記憶體位址V對應的值修改為B。
這樣說或許有些抽象,我們來看一個例子:
1.在記憶體位址V當中,存儲着值為10的變量。
2.此時線程1想要把變量的值增加1。對線程1來說,舊的預期值A=10,要修改的新值B=11。
3.線上程1要送出更新之前,另一個線程2搶先一步,把記憶體位址V中的變量值率先更新成了11。
4.線程1開始送出更新,首先進行A和位址V的實際值比較(Compare),發現A不等于V的實際值,送出失敗。
5.線程1重新擷取記憶體位址V的目前值,并重新計算想要修改的新值。此時對線程1來說,A=11,B=12。這個重新嘗試的過程被稱為自旋。
6.這一次比較幸運,沒有其他線程改變位址V的值。線程1進行Compare,發現A和位址V的實際值是相等的。
7.線程1進行SWAP,把位址V的值替換為B,也就是12。
從思想上來說,
Synchronized屬于悲觀鎖
,悲觀地認為程式中的并發情況嚴重,是以嚴防死守。
CAS屬于樂觀鎖
,樂觀地認為程式中的并發情況不那麼嚴重,是以讓線程不斷去嘗試更新。
4.CAS的缺點
- CPU開銷較大
在并發量比較高的情況下,如果許多線程反複嘗試更新某一個變量,卻又一直更新不成功,循環往複,會給CPU帶來很大的壓力。
- 不能保證代碼塊的原子性
CAS機制所保證的隻是一個變量的原子性操作,而不能保證整個代碼塊的原子性。比如需要保證3個變量共同進行原子性的更新,就不得不使用Synchronized了。
- ABA問題
這是CAS機制最大的問題所在。
什麼是ABA問題?怎麼解決?
5.CAS實作
首先看一看
AtomicInteger
當中常用的自增方法 incrementAndGet:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next)){
return next;
}
}
}
private volatile int value;
public final int get() {
return value;
}
這段代碼是一個無限循環,也就是CAS的自旋。循環體當中做了三件事:
1.擷取目前值。
2.目前值+1,計算出目标值。
3.進行CAS操作,如果成功則跳出循環,如果失敗則重複上述步驟。
這裡需要注意的重點是 get 方法,這個方法的作用是擷取變量的目前值。
如何保證獲得的目前值是記憶體中的最新值呢?很簡單,用volatile關鍵字來保證。
接下來看一看compareAndSet方法的實作,以及方法所依賴對象的來曆:
compareAndSet方法的實作很簡單,隻有一行代碼。這裡涉及到兩個重要的對象,一個是unsafe,一個是valueOffset。
什麼是unsafe呢?Java語言不像C,C++那樣可以直接通路底層作業系統,但是JVM為我們提供了一個後門,這個後門就是unsafe。unsafe為我們提供了硬體級别的原子操作。
至于valueOffset對象,是通過unsafe.objectFieldOffset方法得到,所代表的是AtomicInteger對象value成員變量在記憶體中的偏移量。我們可以簡單地把valueOffset了解為value變量的記憶體位址。
我們上面說過,CAS機制當中使用了3個基本操作數:記憶體位址V,舊的預期值A,要修改的新值B。
而unsafe的compareAndSwapInt方法參數包括了這三個基本元素:valueOffset參數代表了V,expect參數代表了A,update參數代表了B。
正是unsafe的compareAndSwapInt方法保證了Compare和Swap操作之間的原子性操作。
6.ABA問題
什麼是ABA呢?假設記憶體中有一個值為A的變量,存儲在位址V當中。
時有三個線程想使用CAS的方式更新這個變量值,每個線程的執行時間有略微的偏差。線程1和線程2已經獲得目前值,線程3還未獲得目前值。
接下來,線程1先一步執行成功,把目前值成功從A更新為B;同時線程2因為某種原因被阻塞住,沒有做更新操作;線程3線上程1更新之後,獲得了目前值B。
再之後,線程2仍然處于阻塞狀态,線程3繼續執行,成功把目前值從B更新成了A。
最後,線程2終于恢複了運作狀态,由于阻塞之前已經獲得了“目前值”A,并且經過compare檢測,記憶體位址V中的實際值也是A,是以成功把變量值A更新成了B。
這個過程中,線程2擷取到的變量值A是一個舊值,盡管和目前的實際值相同,但記憶體位址V中的變量已經經曆了A->B->A的改變。
7.ABA問題解決
什麼意思呢?真正要做到嚴謹的CAS機制,我們在Compare階段不僅要比較期望值A和位址V中的實際值,還要比較變量的版本号是否一緻。
我們仍然以最初的例子來說明一下,假設位址V中存儲着變量值A,目前版本号是01。線程1獲得了目前值A和版本号01,想要更新為B,但是被阻塞了。
這時候,記憶體位址V中的變量發生了多次改變,版本号提升為03,但是變量值仍然是A。
随後線程1恢複運作,進行Compare操作。經過比較,線程1所獲得的值和位址V的實際值都是A,但是版本号不相等,是以這一次更新失敗。
在Java當中,AtomicStampedReference類就實作了用版本号做比較的CAS機制。