CAS 全稱爲 Compare And Swap
翻譯過來就是比較而且交換
java
在看到 Compare 和 Swap 後,咱們就應該知道,CAS 裏面至少包含了兩個動做,分別是比較和交換,在如今的 CPU 中,爲這兩個動做專門提供了一個指令,就是CAH
指令,由 CPU 來保證這兩個操做必定是原子的,也就是說比較和交換這兩個操做只能是要麼所有完成,要麼所有沒有完成
markdown
CAS 機制中使用了三個操做數,內存地址,舊的預期值,要修改的值;多線程
例如 a + 1 的操做,a 默認=0,app
1,在多個線程修改一個值 a 的時候,會將 a copy 一份到本身的線程內存空間中(預期值),此時預期值就是 a ,要修改的值就是 a+1 的結果,結果就是 1(要修改的值),因爲是多個線程,因此每一個線程內部都會獲得 a = 1。函數
2,接着就會執行比較而且交換, 讓線程中的預期值和主內存中的 a 進行比較,若是相同,就會提交上去,若是不相同,說明 a 的值已經被別的線程修改過了,因此就會提交失敗(這個比較和提交的操做是原子性的)。提交失敗以後,線程就會從新獲取 a 的值,而後重複這一操做。這種重複操做的方式稱爲自旋ui
栗子:this
前提:線程 A,B,主內存中的變量 count = 0;atom
線程A: 要修改 count 值,因此 copy 一份到本身的內存中,而後執行了 + 1 的操做,此時線程A中 count 預期值是 0,要修改的值爲 1spa
線程B :也修改 count 值,也執行了 + 1 的操做,此時線程 B 中 count 的預期值是 0,要修改的值爲 1,線程
線程B :開始提交到主內存了,提交的時候判斷預期值 和 主內存的 count 是同樣的,因此就會提交成功,這時主內存 count =1
線程A :也開始提交了,可是在判斷的時候發現預期值是 0,但主內存是1,不相等,因此,提交失敗,而後就會放棄本次提交。
線程A :提交失敗以後,就與從新執行步驟 1 的操做。
這種方式最終能夠理解成一種 無阻塞的多線程爭搶資源的模型。
ABA
仍是上面的栗子,
在線程 A 執行 + 1,操做的時候,線程 B 已經將 + 1的結果提交的主內存了,可是這個時候他又執行了 - 1的操做提交到主內存,而且這個過程快於線程 A。
這個時候線程 A 進行判斷和交換,發現修改的值和主內存的值相同,而後將計算的結果提交了。
在線程 A 執行的過程當中,線程 B 修改了值,而且將值又修改了回去,雖說結果並無變化,可是值已經被操做過了
這就是典型的 ABA 問題
那麼如何解決呢?
其實解決起來很是簡單,只須要增長一個版本戳便可,在更新值的時候判斷一下版本戳便可。
在 Java 中也有使用版本戳的實現,就是 AtomicMarkableReference
和 AtomicStampedReference
。
AtomicMarkReference
:只關心這個變量有沒有被動過
AtomicStampedReferrence
:不但關心這個變量有麼有動過,而且關心這個變量被動了幾回,例如
val asr = AtomicStampedReference<String>("345", 0)
fun main() {
val oldStamp = asr.stamp
val oldReference = asr.reference
println("$oldReference ---- $oldStamp")
val t1 = Thread {
println("${Thread.currentThread().name} 當前變量值:${asr.reference} 當前版本:${asr.stamp}" +
" ${asr.compareAndSet(asr.reference, "3456", asr.stamp, asr.stamp + 1)}")
}
val t2 = Thread {
println("${Thread.currentThread().name} 當前變量值:${asr.reference} 當前版本:${asr.stamp}" +
" ${asr.compareAndSet(asr.reference, "34567", asr.stamp, asr.stamp + 1)}")
}
t1.start()
t1.join()
t2.start()
t2.join()
println("${asr.reference} ---- ${asr.stamp}")
}
345 ---- 0
Thread-0 當前變量值:345 當前版本:0 true
Thread-1 當前變量值:3456 當前版本:1 true
34567 ---- 2
複製代碼
在修改字符串的時候,要傳入已經修改過的字符串和版本號,負責就會修改錯誤
開銷問題
在 CAS 期間,線程是不會休息的,線程若是長時間沒法提交,內部就一直在進行自旋,這樣就會產生比較大的內存開銷
CAS 只可以保證一個共享變量的原子操做
CAS 只能保證對一個內存地址進行原子操做,因此說使用範圍會有必定限制
例如:若是在執行 a+1 的下面加上,b+1,c +1,這種狀況就會出現問題,這種時候反而使用 Syn 比較方便
其實 Java 中也提供了能夠修改多個變量的原子操做
AtomicReference
:將須要修改的包裝成一個對象,而後使用 AtomicReference
的 compareAndSet 方法進行替換便可
fun main() {
val user = User("張三", 31)
val atomicReference = AtomicReference<User>(user)
println("${atomicReference.get().name} --- ${atomicReference.get().age}")
atomicReference.compareAndSet(user, User("李四", 20))
println("${atomicReference.get().name} --- ${atomicReference.get().age}")
}
class User(val name: String, val age: Int)
複製代碼
上面的代碼就保證了修改多個變量,實際上就是更新對象
在 java 的 atomic 包下,一系列以 Atomic 開頭的包裝類,如 AtomicBoolean
AtomicInteger
等,分別用於 int,bool,long 等類型的原子操做,其原理都是用的 cas
核心實現以下:
//使用將給定函數應用於當前值和給定值的結果,以原子方式更新當前值,並返回更新後的值。 該函數應無反作用,由於當嘗試更新因爲線程間爭用而失敗時,可能會從新應用該函數。 應用該函數時,將當前值做爲其第一個參數,並將給定的update做爲第二個參數
public final int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction) {
//預期值和要更新的值
int prev, next;
do {
//獲取到預期值,也就是當前值
prev = get();
//計算要更新的值
next = accumulatorFunction.applyAsInt(prev, x);
//更新成功則退出循環,不然從新計算
} while (!compareAndSet(prev, next));
return next;
}
//注意:這個比較而且 set 的操做是原子性的
//參數:指望–指望值 更新–新價值
//返回值:
//若是成功,則爲true 。 錯誤返回表示實際值不等於指望值
public final boolean compareAndSet(int expect, int update) {
return U.compareAndSwapInt(this, VALUE, expect, update);
}
複製代碼
若是本文有幫助到你的地方,不勝榮幸,若有文章中有錯誤和疑問,歡迎你們提出!