3. 原子操作與CAS
3.1 原子操作
所謂原子操作是指不會被線程調度機制打斷的操作;這種操作一旦開始,就一直運行到結束,中間不會有任何context switch,也就是切換到另一個線程。
爲了實現原子操作,Java中可以通過synchronized關鍵字將函數或者代碼塊包圍,以實現操作的原子性。但是synchronized關鍵字有一些很顯著的問題:
1、synchronized是基於阻塞鎖的機制,如果被阻塞的線程優先級很高,可能很長時間其他線程都沒有機會運行;
2、拿到鎖的線程一直不釋放鎖,可能導致其他線程一直等待;
3、線程數量很多時,可能帶來大量的競爭,消耗cpu,同時帶來死鎖或者其他安全。
像synchronized這種獨佔鎖屬於悲觀鎖,它是在假設一定會發生衝突的,那麼加鎖恰好有用,除此之外,還有樂觀鎖,樂觀鎖的含義就是假設沒有發生衝突,那麼我正好可以進行某項操作,如果要是發生衝突呢,那我就重試直到成功,樂觀鎖最常見的就是CAS。
JAVA內部在實現原子操作的類時都應用到了CAS。
3.2 CAS
CAS是CompareAndSwap的縮寫,即比較並替換。CAS需要有3個操作數:內存地址V,舊的預期值A,即將要更新的目標值B。
CAS指令執行時,當且僅當內存地址V的值與預期值A相等時,將內存地址V的值修改爲B,否則就什麼都不做。整個比較並替換的操作是一個原子操作,大多數現代處理器都支持CAS指令。
JDK中關於原子操作的類有,
更新基本類型類:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
更新數組類:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
更新引用類型:AtomicReference,AtomicMarkableReference,AtomicStampedReference
原子更新字段類: AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
下面例子爲AtomicInteger的基本用法,
public class UseAtomicInt {
static AtomicInteger ai = new AtomicInteger(10);
public static void main(String[] args) {
System.out.println(ai.getAndIncrement());//10--->11
System.out.println(ai.incrementAndGet());//11--->12--->out
System.out.println(ai.get());
}
}
深入getAndIncrement的實現可以發現,內部調用了CAS進行value的更新,
/**
* Atomically adds the given value to the current value of a field
* or array element within the given object {@code o}
* at the given {@code offset}.
*
* @param o object/array to update the field/element in
* @param offset field/element offset
* @param delta the value to add
* @return the previous value
* @since 1.8
*/
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
CAS在底層通過JNI調用系統的Atomic::cmpxchg方法,來進行value的比較與更新。
對於CAS的實現,這依賴於CPU提供的特定指令,具體根據體系結構的不同還存在着明顯區別。比如,x86 CPU 提供 cmpxchg 指令;而在精簡指令集的體系架構中,則通常是靠一對兒指令(如“load and reserve”和“store conditional”)實現的,在大多數處理器上 CAS 都是個非常輕量級的操作,這也是其優勢所在。
3.3 CAS的問題
CAS雖然很高效的解決了原子操作問題,但是CAS仍然存在三大問題。
- ABA問題
什麼是ABA問題?ABA問題怎麼解決?如果內存地址V初次讀取的值是A,並且在準備賦值的時候檢查到它的值仍然爲A,那我們就能說它的值沒有被其他線程改變過了嗎?如果在這段期間它的值曾經被改成了B,後來又被改回爲A,那CAS操作就會誤認爲它從來沒有被改變過。這個漏洞稱爲CAS操作的“ABA”問題。
Java併發包爲了解決這個問題,提供了一個帶有標記的原子引用類“AtomicStampedReference”,它可以通過控制變量值的版本來保證CAS的正確性。因此,在使用CAS前要考慮清楚“ABA”問題是否會影響程序併發的正確性,如果需要解決ABA問題,改用傳統的互斥同步可能會比原子類更高效。
- 開銷問題
循環時間長開銷很大:我們可以看到getAndAddInt方法執行時,如果CAS失敗,會一直進行嘗試。如果CAS長時間一直不成功,可能會給CPU帶來很大的開銷。
- 只能保證一個共享變量的原子操作
只能保證一個共享變量的原子操作:當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖來保證原子性。
本文由『後端精進之路』原創,首發於博客 http://teckee.github.io/ , 轉載請註明出處
搜索『後端精進之路』關注公衆號,立刻獲取最新文章和價值2000元的BATJ精品面試課程。