專業(yè)建設(shè)網(wǎng)站公司哪家好/優(yōu)化深圳seo
CAS這個機制就給實現(xiàn)線程安全版本的代碼,提供了一個新的思路,之前通過加鎖,把多個指令打包成整體,來實現(xiàn)線程安全?,F(xiàn)在就可以考慮直接基與CAS來實現(xiàn)一些修改操作,也能保證線程安全(不需要加鎖)
1.什么是 CAS
CAS: 全稱Compare and swap,字面意思:”比較并交換“,一個 CAS 涉及到以下操作:
我們假設(shè)內(nèi)存中的原數(shù)據(jù)V,舊的預(yù)期值A(chǔ),需要修改的新值B。
1. 比較 A 與 V 是否相等。(比較)
2. 如果比較相等,將 B 寫入 V。(交換)
3. 返回操作是否成功。
CAS 偽代碼
下面寫的代碼不是原子的, 真實的 CAS 是一個原子的硬件指令完成的. 這個偽代碼只是輔助理解
CAS 的工作流程.
boolean CAS(address, expectValue, swapValue) {if (&address == expectedValue){&address = swapValue;return true;}return false;}
當(dāng)多個線程同時對某個資源進行CAS操作,只能有一個線程操作成功,但是并不會阻塞其他線程,其他線程只會收到操作失敗的信號.
CAS 可以視為是一種樂觀鎖. (或者可以理解成 CAS 是樂觀鎖的一種實現(xiàn)方式)
?
2.CAS 是怎么實現(xiàn)
針對不同的操作系統(tǒng),JVM 用到了不同的 CAS 實現(xiàn)原理,簡單來講:
- java 的 CAS 利用的的是 unsafe 這個類提供的 CAS 操作;
- unsafe 的 CAS 依賴了的是 jvm 針對不同的操作系統(tǒng)實現(xiàn)的 Atomic::cmpxchg;
- Atomic::cmpxchg 的實現(xiàn)使用了匯編的 CAS 操作,并使用 cpu 硬件提供的 lock 機制保證其原子性
簡而言之,是因為硬件予以了支持,軟件層面才能做到.
?
3.CAS 有哪些應(yīng)用
(1)實現(xiàn)原子類
標(biāo)準庫中提供了 java.util.concurrent.atomic 包, 里面的類都是基于這種方式來實現(xiàn)的.
典型的就是 AtomicInteger 類. 其中的 getAndIncrement 相當(dāng)于 i++ 操作.
?
package thread2;import java.util.concurrent.atomic.AtomicInteger;public class Test7 {private static AtomicInteger num = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {num.getAndIncrement();}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {num.getAndIncrement();}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(num.get());}}
?
不會出現(xiàn)線程安全問題!
偽代碼實現(xiàn)getAndIncrement:
class AtomicInteger {private int value;public int getAndIncrement() {int oldValue = value;while (CAS(value, oldValue, oldValue + 1) != true) {oldValue = value;}return oldValue;}
}
假設(shè)兩個線程同時調(diào)用 getAndIncrement
(1)兩個線程都讀取 value 的值到 oldValue 中. (oldValue 是一個局部變量, 在棧上. 每個線程有自己的棧)
?
?(2)?線程1 先執(zhí)行 CAS 操作. 由于 oldValue 和 value 的值相同, 直接進行對 value 賦值.
- CAS 是直接讀寫內(nèi)存的, 而不是操作寄存器.
- CAS 的讀內(nèi)存, 比較, 寫內(nèi)存操作是一條硬件指令, 是原子的.
?
(3)?線程2 再執(zhí)行 CAS 操作, 第一次 CAS 的時候發(fā)現(xiàn) oldValue 和 value 不相等, 不能進行賦值. 因此需要進入循環(huán).在循環(huán)里重新讀取 value 的值賦給 oldValue
?
(4)線程2 接下來第二次執(zhí)行 CAS, 此時 oldValue 和 value 相同, 于是直接執(zhí)行賦值操作.
?
(5) 線程1 和 線程2 返回各自的 oldValue 的值即可.
通過形如上述代碼就可以實現(xiàn)一個原子類. 不需要使用重量級鎖, 就可以高效的完成多線程的自增操作.
本來 check and set 這樣的操作在代碼角度不是原子的. 但是在硬件層面上可以讓一條指令完成這
個操作, 也就變成原子的了.
(2)實現(xiàn)自旋鎖
基于 CAS 實現(xiàn)更靈活的鎖, 獲取到更多的控制權(quán)
public class SpinLock {private Thread owner = null;public void lock() {
// 通過 CAS 看當(dāng)前鎖是否被某個線程持有.
// 如果這個鎖已經(jīng)被別的線程持有, 那么就自旋等待.
// 如果這個鎖沒有被別的線程持有, 那么就把 owner 設(shè)為當(dāng)前嘗試加鎖的線程.while (!CAS(this.owner, null, Thread.currentThread())) {}}public void unlock() {this.owner = null;}
}
通過CAS,比較一下, owner是不是null (問問這個鎖,是不是空閑的,沒人用的)
如果是空閑的,就直接把當(dāng)前線程的值,賦值給這里owner就相當(dāng)于表示了當(dāng)前的鎖被這個線程給獲取到了~~
如果當(dāng)前鎖不是空閑的,此處CAS的比較就會失敗,直接返回false,循環(huán)繼續(xù)再進行一次再重復(fù)上述的CAS過程~
直到owner為 null,這個循環(huán)CAS操作比較交換成功,才能從lock方法中返回~~
自旋鎖的特點,就是在其他線程釋放了鎖的瞬間,就能感知到,并且獲取到鎖~~
4.CAS 的 ABA 問題
假設(shè)存在兩個線程 t1 和 t2. 有一個共享變量 num, 初始值為 A.接下來, 線程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要
- 先讀取 num 的值, 記錄到 oldNum 變量中.
- 使用 CAS 判定當(dāng)前 num 的值是否為 A, 如果為 A, 就修改成 Z
但是, 在 t1 執(zhí)行這兩個操作之間, t2 線程可能把 num 的值從 A 改成了 B, 又從 B 改成了 A
線程 t1 的 CAS 是期望 num 不變就修改. 但是 num 的值已經(jīng)被 t2 給改了. 只不過又改成 A 了. 這個時候 t1 究竟是否要更新 num 的值為 Z 呢?
到這一步, t1 線程無法區(qū)分當(dāng)前這個變量始終是 A, 還是經(jīng)歷了一個變化過程.
ABA 問題引來的 BUG
大部分的情況下, t2 線程這樣的一個反復(fù)橫跳改動, 對于 t1 是否修改 num 是沒有影響的. 但是不排除一些特殊情況
假設(shè) 滑稽老哥 有 100 存款. 滑稽想從 ATM 取 50 塊錢. 取款機創(chuàng)建了兩個線程, 并發(fā)的來執(zhí)行 -50
操作.我們期望一個線程執(zhí)行 -50 成功, 另一個線程 -50 失敗.如果使用 CAS 的方式來完成這個扣款過程就可能出現(xiàn)問題.
正常的過程
- 存款 100. 線程1 獲取到當(dāng)前存款值為 100, 期望更新為 50; 線程2 獲取到當(dāng)前存款值為 100, 期望更新為 50.
- 線程1 執(zhí)行扣款成功, 存款被改成 50. 線程2 阻塞等待中.
- 輪到線程2 執(zhí)行了, 發(fā)現(xiàn)當(dāng)前存款為 50, 和之前讀到的 100 不相同, 執(zhí)行失敗.
異常的過程
- 存款 100. 線程1 獲取到當(dāng)前存款值為 100, 期望更新為 50; 線程2 獲取到當(dāng)前存款值為 100, 期望更新為 50.
- 線程1 執(zhí)行扣款成功, 存款被改成 50. 線程2 阻塞等待中.
- 在線程2 執(zhí)行之前, 滑稽的朋友正好給滑稽轉(zhuǎn)賬 50, 賬戶余額變成 100 !!
- 輪到線程2 執(zhí)行了, 發(fā)現(xiàn)當(dāng)前存款為 100, 和之前讀到的 100 相同, 再次執(zhí)行扣款操作這個時候, 扣款操作被執(zhí)行了兩次!!! 都是 ABA 問題搞的鬼
解決方案
給要修改的值, 引入版本號. 在 CAS 比較數(shù)據(jù)當(dāng)前值和舊值的同時, 也要比較版本號是否符合預(yù)期.
- CAS 操作在讀取舊值的同時, 也要讀取版本號
真正修改的時候,
- 如果當(dāng)前版本號和讀到的版本號相同, 則修改數(shù)據(jù), 并把版本號 + 1.
- 如果當(dāng)前版本號高于讀到的版本號. 就操作失敗(認為數(shù)據(jù)已經(jīng)被修改過了).
?
對比理解上面的轉(zhuǎn)賬例子
假設(shè) 滑稽老哥 有 100 存款. 滑稽想從 ATM 取 50 塊錢. 取款機創(chuàng)建了兩個線程, 并發(fā)的來執(zhí)行 -50
操作.
我們期望一個線程執(zhí)行 -50 成功, 另一個線程 -50 失敗.
為了解決 ABA 問題, 給余額搭配一個版本號, 初始設(shè)為 1.
1.存款 100. 線程1 獲取到 存款值為 100, 版本號為 1, 期望更新為 50; 線程2 獲取到存款值為 100,
版本號為 1, 期望更新為 50.
2.線程1 執(zhí)行扣款成功, 存款被改成 50, 版本號改為2. 線程2 阻塞等待中.
3.在線程2 執(zhí)行之前, 滑稽的朋友正好給滑稽轉(zhuǎn)賬 50, 賬戶余額變成 100, 版本號變成3.
4.輪到線程2 執(zhí)行了, 發(fā)現(xiàn)當(dāng)前存款為 100, 和之前讀到的 100 相同, 但是當(dāng)前版本號為 3, 之前讀
到的版本號為 1, 版本小于當(dāng)前版本, 認為操作失敗.
都版本不一樣了咋辦?線程一是version=2,線程2是version=1,都讀取失敗,重新從內(nèi)存讀取至版本和值。?
在 Java 標(biāo)準庫中提供了 AtomicStampedReference<E> 類. 這個類可以對某個類進行包裝, 在內(nèi)部就提供了上面描述的版本管理功能.
?
1) 講解下你自己理解的 CAS 機制
全稱 Compare and swap, 即 "比較并交換". 相當(dāng)于通過一個原子的操作, 同時完成 "讀取內(nèi)存, 比
較是否相等, 修改內(nèi)存" 這三個步驟. 本質(zhì)上需要 CPU 指令的支撐.
2) ABA問題怎么解決?
給要修改的數(shù)據(jù)引入版本號. 在 CAS 比較數(shù)據(jù)當(dāng)前值和舊值的同時, 也要比較版本號是否符合預(yù)期.
如果發(fā)現(xiàn)當(dāng)前版本號和之前讀到的版本號一致, 就真正執(zhí)行修改操作, 并讓版本號自增; 如果發(fā)現(xiàn)當(dāng)
前版本號比之前讀到的版本號大, 就認為操作失敗
?