日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

高并发必学的 CAS 操作,看这篇就够了!

發布時間:2024/3/24 编程问答 70 豆豆
生活随笔 收集整理的這篇文章主要介紹了 高并发必学的 CAS 操作,看这篇就够了! 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

大家好,我是樹哥。

CAS 操作是高并發場景下,性能如此之高的一個重要優化。今天推薦勝哥的一篇關于 CAS 的文章,帶你了解 CAS 的前世今生,寫得真是太棒了!

背景

在高并發的業務場景下,線程安全問題是必須考慮的,在JDK5之前,可以通過synchronized或Lock來保證同步,從而達到線程安全的目的。但synchronized或Lock方案屬于互斥鎖的方案,比較重量級,加鎖、釋放鎖都會引起性能損耗問題。

而在某些場景下,我們是可以通過JUC提供的CAS機制實現無鎖的解決方案,或者說是它基于類似于樂觀鎖的方案,來達到非阻塞同步的方式保證線程安全。

CAS機制不僅是面試中會高頻出現的面試題,而且也是高并發實踐中必須掌握的知識點。如果你目前對CAS還不甚了解,或許只有模糊的印象,這篇文章一定值得你花時間學習一下。

什么是CAS?

CAS是Compare And Swap的縮寫,直譯就是比較并交換。CAS是現代CPU廣泛支持的一種對內存中的共享數據進行操作的一種特殊指令,這個指令會對內存中的共享數據做原子的讀寫操作。其作用是讓CPU比較內存中某個值是否和預期的值相同,如果相同則將這個值更新為新值,不相同則不做更新。

本質上來講CAS是一種無鎖的解決方案,也是一種基于樂觀鎖的操作,可以保證在多線程并發中保障共享資源的原子性操作,相對于synchronized或Lock來說,是一種輕量級的實現方案。

Java中大量使用了CAS機制來實現多線程下數據更新的原子化操作,比如AtomicInteger、CurrentHashMap當中都有CAS的應用。但Java中并沒有直接實現CAS,CAS相關的實現是借助C/C++調用CPU指令來實現的,效率很高,但Java代碼需通過JNI才能調用。比如,Unsafe類提供的CAS方法(如compareAndSwapXXX)底層實現即為CPU指令cmpxchg。

CAS的基本流程

下面我們用一張圖來了解一下CAS操作的基本流程。

CAS操作流程圖

在上圖中涉及到三個值的比較和操作:修改之前獲取的(待修改)值A,業務邏輯計算的新值B,以及待修改值對應的內存位置的C。

整個處理流程中,假設內存中存在一個變量i,它在內存中對應的值是A(第一次讀取),此時經過業務處理之后,要把它更新成B,那么在更新之前會再讀取一下i現在的值C,如果在業務處理的過程中i的值并沒有發生變化,也就是A和C相同,才會把i更新(交換)為新值B。如果A和C不相同,那說明在業務計算時,i的值發生了變化,則不更新(交換)成B。最后,CPU會將舊的數值返回。而上述的一系列操作由CPU指令來保證是原子的。

在《Java并發編程實踐》中對CAS進行了更加通俗的描述:我認為原有的值應該是什么,如果是,則將原有的值更新為新值,否則不做修改,并告訴我原來的值是多少。

在上述路程中,我們可以很清晰的看到樂觀鎖的思路,而且這期間并沒有使用到鎖。因此,相對于synchronized等悲觀鎖的實現,效率要高非常多。

基于CAS的AtomicInteger使用

關于CAS的實現,最經典最常用的當屬AtomicInteger了,我們馬上就來看一下AtomicInteger是如何利用CAS實現原子性操作的。為了形成更新鮮明的對比,先來看一下如果不使用CAS機制,想實現線程安全我們通常如何處理。

在沒有使用CAS機制時,為了保證線程安全,基于synchronized的實現如下:

public?class?ThreadSafeTest?{public?static?volatile?int?i?=?0;public?synchronized?void?increase()?{i++;} }

至于上面的實例具體實現,這里不再展開,很多相關的文章專門進行講解,我們只需要知道為了保證i++的原子操作,在increase方法上使用了重量級的鎖synchronized,這會導致該方法的性能低下,所有調用該方法的操作都需要同步等待處理。

那么,如果采用基于CAS實現的AtomicInteger類,上述方法的實現便變得簡單且輕量級了:

public?class?ThreadSafeTest?{private?final?AtomicInteger?counter?=?new?AtomicInteger(0);public?int?increase(){return?counter.addAndGet(1);}}

之所以可以如此安全、便捷地來實現安全操作,便是由于AtomicInteger類采用了CAS機制。下面,我們就來了解一下AtomicInteger的功能及源碼實現。

CAS的AtomicInteger類

AtomicInteger是java.util.concurrent.atomic 包下的一個原子類,該包下還有AtomicBoolean, AtomicLong,AtomicLongArray, AtomicReference等原子類,主要用于在高并發環境下,保證線程安全。

AtomicInteger常用API

AtomicInteger類提供了如下常見的API功能:

public final int get():獲取當前的值 public final int getAndSet(int newValue):獲取當前的值,并設置新的值 public final int getAndIncrement():獲取當前的值,并自增 public final int getAndDecrement():獲取當前的值,并自減 public final int getAndAdd(int delta):獲取當前的值,并加上預期的值 void lazySet(int newValue):?最終會設置成newValue,使用lazySet設置值后,可能導致其他線程在之后的一小段時間內還是可以讀到舊的值。

上述方法中,getAndXXX格式的方法都實現了原子操作。具體的使用方法參考上面的addAndGet案例即可。

AtomicInteger核心源碼

下面看一下AtomicInteger代碼中的核心實現代碼:

public?class?AtomicInteger?extends?Number?implements?java.io.Serializable?{private?static?final?Unsafe?unsafe?=?Unsafe.getUnsafe();private?static?final?long?valueOffset;static?{try?{//?用于獲取value字段相對當前對象的“起始地址”的偏移量valueOffset?=?unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));}?catch?(Exception?ex)?{?throw?new?Error(ex);?}}private?volatile?int?value;//返回當前值public?final?int?get()?{return?value;}//遞增加detlapublic?final?int?getAndAdd(int?delta)?{// 1、this:當前的實例?// 2、valueOffset:value實例變量的偏移量?// 3、delta:當前value要加上的數(value+delta)。return?unsafe.getAndAddInt(this,?valueOffset,?delta);}//遞增加1public?final?int?incrementAndGet()?{return?unsafe.getAndAddInt(this,?valueOffset,?1)?+?1;} ... }

上述代碼以AtomicInteger#incrementAndGet方法為例展示了AtomicInteger的基本實現。其中,在static靜態代碼塊中,基于Unsafe類獲取value字段相對當前對象的“起始地址”的偏移量,用于后續Unsafe類的處理。

在處理自增的原子操作時,使用的是Unsafe類中的getAndAddInt方法,CAS的實現便是由Unsafe類的該方法提供,從而保證自增操作的原子性。

同時,在AtomicInteger類中,可以看到value值通過volatile進行修飾,保證了該屬性值的線程可見性。在多并發的情況下,一個線程的修改,可以保證到其他線程立馬看到修改后的值。

通過源碼可以看出, AtomicInteger 底層是通過volatile變量和CAS兩者相結合來保證更新數據的原子性。其中關于Unsafe類對CAS的實現,我們下面詳細介紹。

CAS的工作原理

CAS的實現原理簡單來說就是由Unsafe類和其中的自旋鎖來完成的,下面針對源代碼來看一下這兩塊的內容。

UnSafe類

在AtomicInteger核心源碼中,已經看到CAS的實現是通過Unsafe類來完成的,先來了解一下Unsafe類的作用。關于Unsafe類在之前的文章《各大框架都在使用的Unsafe類,到底有多神奇?》也有詳細的介紹,大家可以參考,這里我們再簡單概述一下。

sun.misc.Unsafe是JDK內部用的工具類。它通過暴露一些Java意義上說“不安全”的功能給Java層代碼,來讓JDK能夠更多的使用Java代碼來實現一些原本是平臺相關的、需要使用native語言(例如C或C++)才可以實現的功能。該類不應該在JDK核心類庫之外使用,這也是命名為Unsafe(不安全)的原因。

JVM的實現可以自由選擇如何實現Java對象的“布局”,也就是在內存里Java對象的各個部分放在哪里,包括對象的實例字段和一些元數據之類。

Unsafe里關于對象字段訪問的方法把對象布局抽象出來,它提供了objectFieldOffset()方法用于獲取某個字段相對Java對象的“起始地址”的偏移量,也提供了getInt、getLong、getObject之類的方法可以使用前面獲取的偏移量來訪問某個Java對象的某個字段。在AtomicInteger的static代碼塊中便使用了objectFieldOffset()方法。

Unsafe類的功能主要分為內存操作、CAS、Class相關、對象操作、數組相關、內存屏障、系統相關、線程調度等功能。這里我們只需要知道其功能即可,方便理解CAS的實現,注意不建議在日常開發中使用。

Unsafe與CAS

AtomicInteger調用了Unsafe#getAndAddInt方法:

public?final?int?incrementAndGet()?{return?unsafe.getAndAddInt(this,?valueOffset,?1)?+?1;}

上述代碼等于是AtomicInteger調用UnSafe類的CAS方法,JVM幫我們實現出匯編指令,從而實現原子操作。

在Unsafe中getAndAddInt方法實現如下:

public?final?int?getAndAddInt(Object?var1,?long?var2,?int?var4)?{int?var5;do?{var5?=?this.getIntVolatile(var1,?var2);}?while(!this.compareAndSwapInt(var1,?var2,?var5,?var5?+?var4));return?var5;}

getAndAddInt方法有三個參數:

  • 第一個參數表示當前對象,也就是new的那個AtomicInteger對象;

  • 第二個表示內存地址;

  • 第三個表示自增步伐,在AtomicInteger#incrementAndGet中默認的自增步伐是1。

getAndAddInt方法中,首先把當前對象主內存中的值賦給val5,然后進入while循環。判斷當前對象此刻主內存中的值是否等于val5,如果是,就自增(交換值),否則繼續循環,重新獲取val5的值。

在上述邏輯中核心方法是compareAndSwapInt方法,它是一個native方法,這個方法匯編之后是CPU原語指令,原語指令是連續執行不會被打斷的,所以可以保證原子性。

在getAndAddInt方法中還涉及到一個實現自旋鎖。所謂的自旋,其實就是上面getAndAddInt方法中的do while循環操作。當預期值和主內存中的值不等時,就重新獲取主內存中的值,這就是自旋。

這里我們可以看到CAS實現的一個缺點:內部使用自旋的方式進行CAS更新(while循環進行CAS更新,如果更新失敗,則循環再次重試)。如果長時間都不成功的話,就會造成CPU極大的開銷。

另外,Unsafe類還支持了其他的CAS方法,比如compareAndSwapObject、 compareAndSwapInt、compareAndSwapLong。更多關于Unsafe類的功能就不再展開,大家可以去看《各大框架都在使用的Unsafe類,到底有多神奇?》這篇文章。

CAS的缺點

CAS高效地實現了原子性操作,但在以下三方面還存在著一些缺點:

  • 循環時間長,開銷大;

  • 只能保證一個共享變量的原子操作;

  • ABA問題;

下面就這個三個問題詳細討論一下。

循環時間長開銷大

在分析Unsafe源代碼的時候我們已經提到,在Unsafe的實現中使用了自旋鎖的機制。在該環節如果CAS操作失敗,就需要循環進行CAS操作(do while循環同時將期望值更新為最新的),如果長時間都不成功的話,那么會造成CPU極大的開銷。如果JVM能支持處理器提供的pause指令那么效率會有一定的提升。

只能保證一個共享變量的原子操作

在最初的實例中,可以看出是針對一個共享變量使用了CAS機制,可以保證原子性操作。但如果存在多個共享變量,或一整個代碼塊的邏輯需要保證線程安全,CAS就無法保證原子性操作了,此時就需要考慮采用加鎖方式(悲觀鎖)保證原子性,或者有一個取巧的辦法,把多個共享變量合并成一個共享變量進行CAS操作。

ABA問題

雖然使用CAS可以實現非阻塞式的原子性操作,但是會產生ABA問題,ABA問題出現的基本流程:

  • 進程P1在共享變量中讀到值為A;

  • P1被搶占了,進程P2執行;

  • P2把共享變量里的值從A改成了B,再改回到A,此時被P1搶占;

  • P1回來看到共享變量里的值沒有被改變,于是繼續執行;

雖然P1以為變量值沒有改變,繼續執行了,但是這個會引發一些潛在的問題。ABA問題最容易發生在lock free的算法中的,CAS首當其沖,因為CAS判斷的是指針的地址。如果這個地址被重用了呢,問題就很大了(地址被重用是很經常發生的,一個內存分配后釋放了,再分配,很有可能還是原來的地址)。

維基百科上給了一個形象的例子:你拿著一個裝滿錢的手提箱在飛機場,此時過來了一個火辣性感的美女,然后她很暖昧地挑逗著你,并趁你不注意,把用一個一模一樣的手提箱和你那裝滿錢的箱子調了個包,然后就離開了,你看到你的手提箱還在那,于是就提著手提箱去趕飛機去了。

ABA問題的解決思路就是使用版本號:在變量前面追加上版本號,每次變量更新的時候把版本號加1,那么A->B->A就會變成1A->2B->3A。

另外,從Java 1.5開始,JDK的Atomic包里提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法的作用是首先檢查當前引用是否等于預期引用,并且檢查當前標志是否等于預期標志,如果全部相等,則以原子方式將該引用和該標志的值設置為給定的更新值。

小結

本文從CAS的基本使用場景、基本流程、實現類AtomicInteger源碼解析、CAS的Unsafe實現解析、CAS的缺點及解決方案等方面來全面了解了CAS。通過這篇文章的學習想必你已經更加深刻的理解CAS機制了,如果對你有所幫助,記得關注一下,持續輸出干貨內容。

看完文章覺得沒弄懂

想和更多技術小伙伴交流?

掃碼添加樹哥微信,回復「入群」即可加入社群!


推薦閱讀

  • 一次線上事故,我頓悟了異步的精髓!

  • Java 中為什么不全部使用 static 方法?

  • 一文帶你弄懂 CDN 的技術原理!

  • 如何設計一個分布式 ID 發號器?

  • 關于 CMS 垃圾回收器,你真的懂了嗎?

  • 接口流量突增,如何做好性能優化?

  • MySQL 啥時候用表鎖,啥時候用行鎖?

  • 從全局角度,如何設計一個秒殺系統?

  • 系統總出故障怎么辦,或許你該學學穩定性建設!

  • 服務器宕機了,Kafka 消息會丟失嗎?

總結

以上是生活随笔為你收集整理的高并发必学的 CAS 操作,看这篇就够了!的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。