Java中的CAS理解

在JDK 5之前Java語言是靠synchronized關鍵字保證同步的,這會導致有鎖

鎖機制存在以下問題:

(1)在多線程競爭下,加鎖、釋放鎖會導致比較多的上下文切換和調度延時,引起性能問題。

(2)一個線程持有鎖會導致其它所有需要此鎖的線程掛起。

(3)如果一個優先級高的線程等待一個優先級低的線程釋放鎖會導致優先級倒置,引起性能風險。

volatile是不錯的機制,但是volatile不能保證原子性。因此對於同步最終還是要回到鎖機制上來。

獨佔鎖是一種悲觀鎖,synchronized就是一種獨佔鎖,會導致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。而另一個更加有效的鎖就是樂觀鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止。樂觀鎖用到的機制就是CAS,Compare and Swap。

CAS是什麼?

cas是compareandswap的簡稱,從字面上理解就是比較並交換,簡單來說:從某一內存上取值V,和預期值A進行比較,如果內存值V和預期值A的結果相等,那麼我們就把新值B更新到內存,如果不相等,那麼就重複上述操作直到成功爲止。

CAS能做什麼?

上面我們瞭解了cas是什麼了,那麼它能解決什麼問題呢?它可以解決多線程併發安全的問題,以前我們對一些多線程操作的代碼都是使用synchronize關鍵字,來保證線程安全的問題;現在我們將cas放入到多線程環境裏我們看一下它是怎麼解決的,我們假設有A、B兩個線程同時執行一個int值value自增的代碼,並且同時獲取了當前的value,我們還要假設線程B比A快了那麼0.00000001s,所以B先執行,線程B執行了cas操作之後,發現當前值和預期值相符,就執行了自增操作,此時這個value = value + 1;然後A開始執行,A也執行了cas操作,但是此時value的值和它當時取到的值已經不一樣了,所以此次操作失敗,重新取值然後比較成功,然後將value值更新,這樣兩個線程進入,value值自增了兩次,符合我們的預期。

CAS在java中的應用

是不是感覺cas很好用,那麼在java中有對應的實現嗎?有的!java從jdk1.5就將cas引入並且使用了,java中的Atomic系列就是使用cas實現的,下面我們就用AtomicInteger類看一下java是怎麼實現的吧。
在這裏插入圖片描述
進入到AtomicInteger類裏邊之後,我們發現它使用volatile聲明瞭一個變量,至於volatile有什麼特性,我就不詳細贅述了,簡單來說volatile聲明這個變量是易變的,當線程拿到這個值並且更新之後還要將更新後的值同步到主內存裏邊,供之後的線程調用。

好了,瞭解了volatile的特性之後,我們再來看一下它怎麼實現自增的吧。
在這裏插入圖片描述
AtomicInteger有一個incrementAndGet的自增方法,在一個循環裏,每次去獲取當前的值current,然後將當前值current+1賦值給next,然後將current和next放到compareAndSet中進行比較,如果返回true那麼就return next的值,如果失敗,那麼繼續進行上述操作,是不是很眼熟這個操作,是的,你沒看錯,這裏就是使用了cas操作,看到這裏是不是感覺java很直白,哈哈。

好的,我們再來看compareAndSet是不是如我們所想的那樣使用了cas呢,我們再進入compareAndSet方法中一探究竟。
在這裏插入圖片描述
可以看到這個compareAndSet方法有兩個參數,分別叫expect和update,從字面上理解就是預期的值和更新的值,OK,我們再來看裏邊,裏邊調用了一個compareAndSwapInt的方法,有四個參數分別是當前的值this、valueOffset、預期值expect、更新的值update,其中expect和update是通過參數傳過來的,你們還記得這兩個值分別是什麼嗎?不記得的童鞋們請網上翻,沒錯就是incrementAndGet方法中的current和next,好的,我們來梳理一下this當前值和預期值expect也就是current進行比較,如果相等,就把值更新爲update也就是next,這樣此次自增操作完成!至於valueOffset容我買個關子。

CAS有沒有什麼不好的隱患呢?

毫無疑問肯定有的!

1、首先就是經典的ABA問題

何爲ABA呢?我們還是以兩個線程L、N進行自增操作爲例,線程L、N同時獲取當前的值A,只不過此時線程N比較快,它在L操作之前,進行了兩次操作,第一次將值從A 改爲了B,之後又將B改爲了A,那麼在線程L操作的 時候發現當前的值還是A,符合預期,那麼它也會更新成功,從操作上看並沒有什麼不對,更新成功也是對的,但是這樣是有隱患的,這個網上有好多關於ABA問題隱患的解讀,我覺得有一個老哥使用鏈表的表述最爲貼切,這個是我很久之前看的,我現在也找不到這個老哥關於這個問題解讀的帖子了,大家自行搜索一下吧,爲了解決這個問題,java引入了版本的概念,相當於上述操作變爲了A1----B2----A3,這樣就非常明確了,這個版本相信大家也猜到那就是valueOffset,所以在AtomicInteger中進行cas操作時除了this、expect、update之外還有一個valueOffset的參數進行版本的區分,就是爲了解決ABA問題的。

2、長時間自旋非常消耗資源

先說一下什麼叫自旋,自旋就是cas的一個操作週期,如果一個線程特別倒黴,每次獲取的值都被其他線程的修改了,那麼它就會一直進行自旋比較,直到成功爲止,在這個過程中cpu的開銷十分的大,所以要儘量避免。

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

當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操作。比如有兩個共享變量i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象裏來進行CAS操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章