crm辦公系統(tǒng)武漢關(guān)鍵詞seo
CAS 詳解
- 一. 什么是 CAS
- 二. CAS 的應(yīng)用
- 1. 實(shí)現(xiàn)原子類
- 2. 實(shí)現(xiàn)自旋鎖
- 三. CAS 的 ABA 問題
- 四. 相關(guān)面試題
一. 什么是 CAS
CAS: 全稱Compare and swap,字面意思:”比較并交換“
一個(gè) CAS 涉及到以下操作:
我們假設(shè)內(nèi)存中的原數(shù)據(jù) V,舊的預(yù)期值 A,需要修改的新值 B。
- 比較 A 與 V 是否相等。(比較)
- 如果比較相等,將 B 寫入 V。(交換)
- 返回操作是否成功。
CAS 偽代碼:
下面寫的代碼不是原子的, 真實(shí)的 CAS 是一個(gè)原子的硬件指令完成的.
因?yàn)楸旧砭褪窃硬僮? 就沒有線程安全問題,
boolean CAS(address, expectValue, swapValue) {// 取出 address 的值if (&address == expectedValue) {&address = swapValue;return true;}return false;}
當(dāng)多個(gè)線程同時(shí)對(duì)某個(gè)資源進(jìn)行CAS操作,只能有一個(gè)線程操作成功,但是并不會(huì)阻塞其他線程, 其他線程只會(huì)收到操作失敗的信號(hào)。
可以理解為: CAS 是樂觀鎖的一種實(shí)現(xiàn)方式
二. CAS 的應(yīng)用
CAS 最大的意義: 為我們寫多線程安全的代碼提供新的方向和思路
1. 實(shí)現(xiàn)原子類
標(biāo)準(zhǔn)庫(kù)中提供了 java.util.concurrent.atomic 包, 里面的類都是基于這種方式來實(shí)現(xiàn)的.
典型的就是 AtomicInteger 類. 其中的 getAndIncrement 相當(dāng)于 i++ 操作.
AtomicInteger atomicInteger = new AtomicInteger(0);
// 相當(dāng)于 i++
atomicInteger.getAndIncrement();
偽代碼實(shí)現(xiàn):
class AtomicInteger {private int value;public int getAndIncrement() {int oldValue = value;while ( CAS(value, oldValue, oldValue+1) != true) {// 沒有成功就將期待值換為最新值oldValue = value;}return oldValue;}
}
通過形如上述代碼就可以實(shí)現(xiàn)一個(gè)原子類. 不需要使用重量級(jí)鎖, 就可以高效的完成多線程的自增操作, 技能保證線程安全又高效, 因?yàn)?加鎖就涉及到鎖競(jìng)爭(zhēng), 而 CAS 不涉及線程阻塞.
本來 check and set 這樣的操作在代碼角度不是原子的. 但是在硬件層面上可以讓一條指令完成這個(gè)操作, 也就變成原子的了.
2. 實(shí)現(xiàn)自旋鎖
自旋鎖偽代碼
public class SpinLock {private Thread owner = null;public void lock(){// 通過 CAS 看當(dāng)前鎖是否被某個(gè)線程持有. // 如果這個(gè)鎖已經(jīng)被別的線程持有, 那么就自旋等待. // 如果這個(gè)鎖沒有被別的線程持有, 那么就把 owner 設(shè)為當(dāng)前嘗試加鎖的線程. while(!CAS(this.owner, null, Thread.currentThread())){}}public void unlock (){this.owner = null;}
}
三. CAS 的 ABA 問題
ABA 問題:
CAS 關(guān)鍵是先比較再交換, 比較實(shí)質(zhì)是比較當(dāng)前值和舊值是否相同, 若相同, 則視為中間沒有發(fā)生改變, 就進(jìn)行修改. 但是這存在漏洞, 可能中間值變化了又變回來了, CAS 就無法判斷到底是否發(fā)生過改變.
假設(shè)存在兩個(gè)線程 t1 和 t2. 有一個(gè)共享變量 num, 初始值為 A.
接下來, 線程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要
- 先讀取 num 的值, 記錄到 oldNum 變量中.
- 使用 CAS 判定當(dāng)前 num 的值是否為 A, 如果為 A, 就修改成 Z.
但是, 在 t1 執(zhí)行這兩個(gè)操作之間, t2 線程可能把 num 的值從 A 改成了 B, 又從 B 改成了 A
線程 t1 的 CAS 是期望 num 不變就修改. 但是 num 的值已經(jīng)被 t2 給改了. 只不過又改成 A 了. 這
個(gè)時(shí)候 t1 究竟是否要更新 num 的值為 Z 呢?
t1 線程無法區(qū)分當(dāng)前這個(gè)變量始終是 A, 還是經(jīng)歷了一個(gè)變化過程又變?yōu)?A
這就好比, 我們買一個(gè)手機(jī), 無法判定這個(gè)手機(jī)是剛出廠的新手機(jī), 還是別人用舊了, 又翻新過的手
機(jī).
ABA 問題引來的 BUG:
- 假設(shè) A 有 100 存款. 想轉(zhuǎn)賬給 B 50 塊錢. 但是點(diǎn)擊轉(zhuǎn)賬按鈕時(shí)沒反應(yīng), 于是 A 又點(diǎn)擊了一次
- 但是取款機(jī)實(shí)際創(chuàng)建了兩個(gè)線程, 并發(fā)的來執(zhí)行 -50 操作.
- 我們期望一個(gè)線程執(zhí)行 -50 成功, 另一個(gè)線程 -50 失敗.
如果使用 CAS 的方式來完成這個(gè)扣款過程就可能出現(xiàn)問題:
- 正常情況,第二個(gè) 線程 CAS 期望值是 100, 但實(shí)際是 50, 所以扣款失敗.
- 但是如果轉(zhuǎn)賬過程中 C 又 給 A 轉(zhuǎn)帳 50, 所以第二個(gè)線程 CAS 去判斷余額還是 100, 所以就會(huì)扣款成功.
解決方案: 引入版本號(hào)
給要修改的值, 引入版本號(hào). 在 CAS 比較數(shù)據(jù)當(dāng)前值和舊值的同時(shí), 也要比較版本號(hào)是否符合預(yù)期.
- CAS 操作在讀取舊值的同時(shí), 也要讀取版本號(hào).
真正修改的時(shí)候,
- 如果當(dāng)前版本號(hào)和讀到的版本號(hào)相同, 則修改數(shù)據(jù), 并把版本號(hào) + 1.
- 如果當(dāng)前版本號(hào)高于讀到的版本號(hào). 就操作失敗(認(rèn)為數(shù)據(jù)已經(jīng)被修改過了)
四. 相關(guān)面試題
-
講解下你自己理解的 CAS 機(jī)制
全稱 Compare and swap, 即 “比較并交換”. 相當(dāng)于通過一個(gè)原子的操作, 同時(shí)完成 “讀取內(nèi)存, 比
較是否相等, 修改內(nèi)存” 這三個(gè)步驟. 本質(zhì)上需要 CPU 指令的支撐. -
ABA問題怎么解決?
給要修改的數(shù)據(jù)引入版本號(hào). 在 CAS 比較數(shù)據(jù)當(dāng)前值和舊值的同時(shí), 也要比較版本號(hào)是否符合預(yù)期.
如果發(fā)現(xiàn)當(dāng)前版本號(hào)和之前讀到的版本號(hào)一致, 就真正執(zhí)行修改操作, 并讓版本號(hào)自增; 如果發(fā)現(xiàn)當(dāng)前版本號(hào)比之前讀到的版本號(hào)大, 就操作失敗. -
相對(duì)于 鎖機(jī)制, 為什么 CAS 效率更高?
因?yàn)?CAS 本身就是一組原子指令, 不涉及線程安全問題, 所以不需要加鎖、解鎖等開銷,減少了線程在互斥同步時(shí)所產(chǎn)生的性能消耗,當(dāng)多個(gè)線程同時(shí)進(jìn)行 CAS 操作時(shí),只有一個(gè)能成功,其余則需重試,不涉及線程阻塞.
好啦! 以上就是對(duì) CAS 的講解,希望能幫到你 !
評(píng)論區(qū)歡迎指正 !