深入講解我們說的CAS自旋鎖到底是什么
什么是自旋鎖
說道自旋鎖就要從多線程下的鎖機(jī)制說起,由于在多處理器系統(tǒng)環(huán)境中有些資源因?yàn)槠溆邢扌?,有時(shí)需要互斥訪問(mutual exclusion),這時(shí)會引入鎖的機(jī)制,只有獲取了鎖的進(jìn)程才能獲取資源訪問。即每次只能有且只有一個(gè)進(jìn)程能獲取鎖,才能進(jìn)入自己的臨界區(qū),同一時(shí)間不能兩個(gè)或兩個(gè)以上進(jìn)程進(jìn)入臨界區(qū),當(dāng)退出臨界區(qū)時(shí)釋放鎖。
設(shè)計(jì)互斥算法時(shí)總是會面臨一種情況,即沒有獲得鎖的進(jìn)程怎么辦?
通常有2種處理方式:
一種是沒有獲得鎖的調(diào)用者就一直循環(huán)在那里看是否該自旋鎖的保持者已經(jīng)釋放了鎖,這就是本文的重點(diǎn)——自旋鎖。他不用將線城阻塞起來(NON-BLOCKING)。
另一種是沒有獲得鎖的進(jìn)程就阻塞(BLOCKING)自己,繼續(xù)執(zhí)行線程上的其他任務(wù),這就是 ——互斥鎖(包括內(nèi)置鎖Synchronized還有ReentrantLock等等)。
引言
CAS(Compare and swap),即比較并交換,也是實(shí)現(xiàn)我們平時(shí)所說的自旋鎖或樂觀鎖的核心操作。
它的實(shí)現(xiàn)很簡單,就是用一個(gè)預(yù)期的值和內(nèi)存值進(jìn)行比較,如果兩個(gè)值相等,就用預(yù)期的值替換內(nèi)存值,并返回 true。否則,返回 false。
保證原子操作
任何技術(shù)的出現(xiàn)都是為了解決某些特定的問題, CAS 要解決的問題就是保證原子操作。原子操作是什么,原子就是最小不可拆分的,原子操作就是最小不可拆分的操作,也就是說操作一旦開始,就不能被打斷,知道操作完成。在多線程環(huán)境下,原子操作是保證線程安全的重要手段。舉個(gè)例子來說,假設(shè)有兩個(gè)線程在工作,都想對某個(gè)值做修改,就拿自增操作來說吧,要對一個(gè)整數(shù) i 進(jìn)行自增操作,需要基本的三個(gè)步驟:
1、讀取 i 的當(dāng)前值;
2、對 i 值進(jìn)行加 1 操作;
3、將 i 值寫回內(nèi)存;
假設(shè)兩個(gè)進(jìn)程都讀取了 i 的當(dāng)前值,假設(shè)是 0,這時(shí)候 A 線程對 i 加 1 了,B 線程也 加 1,最后 i 的是 1 ,而不是 2。這就是因?yàn)樽栽霾僮鞑皇窃硬僮鳎殖傻倪@三個(gè)步驟可以被干擾。如下面這個(gè)例子,10個(gè)線程,每個(gè)線程都執(zhí)行 10000 次 i++ 操作,我們期望的值是 100,000,但是很遺憾,結(jié)果總是小于 100,000 的。
static int i = 0; public static void add(){ i++; } private static class Plus implements Runnable{ @Override public void run(){ for(int k = 0;k<10000;k++){ add(); } } } public static void main(String[] args) throws InterruptedException{ Thread[] threads = new Thread[10]; for(int i = 0;i<10;i++){ threads[i] = new Thread(new Plus()); threads[i].start(); } for(int i = 0;i<10;i++){ threads[i].join(); } System.out.println(i); }
既然這樣,那怎么辦。沒錯(cuò),也許你已經(jīng)想到了,可以加鎖或者利用 synchronized 實(shí)現(xiàn),例如,將 add() 方法修改為如下這樣:
public synchronized static void add(){ i++; }
或者,加鎖操作,例如下面使用 ReentrantLock (可重入鎖)實(shí)現(xiàn)。
private static Lock lock = new ReentrantLock(); public static void add(){ lock.lock(); i++; lock.unlock(); }
CAS 實(shí)現(xiàn)自旋鎖
既然用鎖或 synchronized 關(guān)鍵字可以實(shí)現(xiàn)原子操作,那么為什么還要用 CAS 呢,因?yàn)榧渔i或使用 synchronized 關(guān)鍵字帶來的性能損耗較大,而用 CAS 可以實(shí)現(xiàn)樂觀鎖,它實(shí)際上是直接利用了 CPU 層面的指令,所以性能很高。
上面也說了,CAS 是實(shí)現(xiàn)自旋鎖的基礎(chǔ),CAS 利用 CPU 指令保證了操作的原子性,以達(dá)到鎖的效果,至于自旋呢,看字面意思也很明白,自己旋轉(zhuǎn),翻譯成人話就是循環(huán),一般是用一個(gè)無限循環(huán)實(shí)現(xiàn)。這樣一來,一個(gè)無限循環(huán)中,執(zhí)行一個(gè) CAS 操作,當(dāng)操作成功,返回 true 時(shí),循環(huán)結(jié)束;當(dāng)返回 false 時(shí),接著執(zhí)行循環(huán),繼續(xù)嘗試 CAS 操作,直到返回 true。
其實(shí) JDK 中有好多地方用到了 CAS ,尤其是java.util.concurrent包下,比如 CountDownLatch、Semaphore、ReentrantLock 中,再比如 java.util.concurrent.atomic 包下,相信大家都用到過 Atomic* ,比如 AtomicBoolean、AtomicInteger 等。
這里拿 AtomicBoolean 來舉個(gè)例子,因?yàn)樗銐蚝唵巍?/p>
public class AtomicBoolean implements java.io.Serializable { private static final long serialVersionUID = 4654671469794556979L; // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicBoolean.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; public final boolean get() { return value != 0; } public final boolean compareAndSet(boolean expect, boolean update) { int e = expect ? 1 : 0; int u = update ? 1 : 0; return unsafe.compareAndSwapInt(this, valueOffset, e, u); } }
這是 AtomicBoolean 的部分代碼,我們看到這里面又幾個(gè)關(guān)鍵方法和屬性。
1、使用了 sun.misc.Unsafe 對象,這個(gè)類提供了一系列直接操作內(nèi)存對象的方法,只是在 jdk 內(nèi)部使用,不建議開發(fā)者使用;
2、value 表示實(shí)際值,可以看到 get 方法實(shí)際是根據(jù) value 是否等于0來判斷布爾值的,這里的 value 定義為 volatile,因?yàn)?volatile 可以保證內(nèi)存可見性,也就是 value 值只要發(fā)生變化,其他線程是馬上可以看到變化后的值的;下一篇會講一下 volatile 可見性問題,歡迎關(guān)注
3、valueOffset 是 value 值的內(nèi)存偏移量,用 unsafe.objectFieldOffset 方法獲得,用作后面的 compareAndSet 方法;
4、compareAndSet 方法,這就是實(shí)現(xiàn) CAS 的核心方法了,在使用 AtomicBoolean 的這個(gè)方法時(shí),只需要傳遞期望值和待更新的值即可,而它里面調(diào)用了 unsafe.compareAndSwapInt(this, valueOffset, e, u) 方法,它是個(gè) native 方法,用 c++ 實(shí)現(xiàn),具體的代碼就不貼了,總之是利用了 CPU 的 cmpxchg 指令完成比較并替換,當(dāng)然根據(jù)具體的系統(tǒng)版本不同,實(shí)現(xiàn)起來也有所區(qū)別,感興趣的可以自行搜一下相關(guān)文章。
使用場景
- CAS 適合簡單對象的操作,比如布爾值、整型值等;
- CAS 適合沖突較少的情況,如果太多線程在同時(shí)自旋,那么長時(shí)間循環(huán)會導(dǎo)致 CPU 開銷很大;
比如 AtomicBoolean 可以用在這樣一個(gè)場景下,系統(tǒng)需要根據(jù)一個(gè)布爾變量的狀態(tài)屬性來判斷是否需要執(zhí)行一些初始化操作,如果是多線程的環(huán)境下,避免多次重復(fù)執(zhí)行,可以使用 AtomicBoolean 來實(shí)現(xiàn),偽代碼如下:
private final static AtomicBoolean flag = new AtomicBoolean(); if(flag.compareAndSet(false,true)){ init(); }
比如 AtomicInteger 可以用在計(jì)數(shù)器中,多線程環(huán)境中,保證計(jì)數(shù)準(zhǔn)確。
ABA問題
CAS 存在一個(gè)問題,就是一個(gè)值從 A 變?yōu)?B ,又從 B 變回了 A,這種情況下,CAS 會認(rèn)為值沒有發(fā)生過變化,但實(shí)際上是有變化的。對此,并發(fā)包下倒是有 AtomicStampedReference 提供了根據(jù)版本號判斷的實(shí)現(xiàn),可以解決一部分問題。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
BigDecimal的toString()、toPlainString()和toEngineeringString()區(qū)
使用BigDecimal進(jìn)行打印的時(shí)候,經(jīng)常會對BigDecimal提供的三個(gè)toString方法感到好奇,以下整理3個(gè)toString方法的區(qū)別及用法,需要的朋友可以參考下2023-08-08SpringMVC攔截器和異常處理器使用示例超詳細(xì)講解
攔截器(Interceptor)是一種動態(tài)攔截方法調(diào)用的機(jī)制,在SpringMVC中動態(tài)攔截控制器方法的執(zhí)行。本文將詳細(xì)講講SpringMVC中攔截器參數(shù)及攔截器鏈配置,感興趣的可以嘗試一下2022-09-09SpringBoot項(xiàng)目中訪問HTML頁面的實(shí)現(xiàn)示例
本文主要介紹了SpringBoot項(xiàng)目中訪問HTML頁面的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-08-08利用spring-data-redis實(shí)現(xiàn)incr自增的操作
這篇文章主要介紹了利用spring-data-redis實(shí)現(xiàn)incr自增的操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11springMVC之HandlerExceptionResolver使用
這篇文章主要介紹了springMVC之HandlerExceptionResolver使用,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11SpringMVC+Mybatis二維碼實(shí)現(xiàn)多平臺付款(附源碼)
本文主要實(shí)現(xiàn)微信支付寶等支付平臺合多為一的二維碼支付,并且實(shí)現(xiàn)有效時(shí)間內(nèi)支付有效,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08SpringBoot整合MQTT并實(shí)現(xiàn)異步線程調(diào)用的問題
這篇文章主要介紹了基于SpringBoot通過注解實(shí)現(xiàn)對mqtt消息處理的異步調(diào)用,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-11-11SpringBoot中配置Web靜態(tài)資源路徑的方法
這篇文章主要介紹了SpringBoot中配置Web靜態(tài)資源路徑的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09Elasticsearch查詢之Match Query示例詳解
這篇文章主要為大家介紹了Elasticsearch查詢之Match查詢示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04