Java中CAS原理詳解

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

鎖機制存在如下問題:java

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

(2)一個線程持有鎖會致使其它全部須要此鎖的線程掛起。服務器

(3)若是一個優先級高的線程等待一個優先級低的線程釋放鎖會致使優先級倒置,引發性能風險。數據結構

volatile是不錯的機制,可是volatile不能保證原子性。所以對於同步最終仍是要回到鎖機制上來。多線程

獨佔鎖是一種悲觀鎖,synchronized就是一種獨佔鎖,會致使其它全部須要鎖的線程掛起,等待持有鎖的線程釋放鎖。而另外一個更加有效的鎖就是樂觀鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止。樂觀鎖用到的機制就是CAS,Compare and Swap。併發

1、什麼是CAS異步

CAS,compare and swap的縮寫,中文翻譯成比較並交換。性能

咱們都知道,在java語言以前,併發就已經普遍存在並在服務器領域獲得了大量的應用。因此硬件廠商老早就在芯片中加入了大量直至併發操做的原語,從而在硬件層面提高效率。在intel的CPU中,使用cmpxchg指令。atom

在Java發展初期,java語言是不可以利用硬件提供的這些便利來提高系統的性能的。而隨着java不斷的發展,Java本地方法(JNI)的出現,使得java程序越過JVM直接調用本地方法提供了一種便捷的方式,於是java在併發的手段上也多了起來。而在Doug Lea提供的cucurenct包中,CAS理論是它實現整個java包的基石。

CAS 操做包含三個操做數 —— 內存位置(V)、預期原值(A)和新值(B)。 若是內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值 。不然,處理器不作任何操做。不管哪一種狀況,它都會在 CAS 指令以前返回該 位置的值。(在 CAS 的一些特殊狀況下將僅返回 CAS 是否成功,而不提取當前 值。)CAS 有效地說明了「我認爲位置 V 應該包含值 A;若是包含該值,則將 B 放到這個位置;不然,不要更改該位置,只告訴我這個位置如今的值便可。」

一般將 CAS 用於同步的方式是從地址 V 讀取值 A,執行多步計算來得到新 值 B,而後使用 CAS 將 V 的值從 A 改成 B。若是 V 處的值還沒有同時更改,則 CAS 操做成功。

相似於 CAS 的指令容許算法執行讀-修改-寫操做,而無需懼怕其餘線程同時 修改變量,由於若是其餘線程修改變量,那麼 CAS 會檢測它(並失敗),算法 能夠對該操做從新計算。

2、CAS的目的

利用CPU的CAS指令,同時藉助JNI來完成Java的非阻塞算法。其它原子操做都是利用相似的特性完成的。而整個J.U.C都是創建在CAS之上的,所以對於synchronized阻塞算法,J.U.C在性能上有了很大的提高。

3、CAS存在的問題

CAS雖然很高效的解決原子操做,可是CAS仍然存在三大問題。ABA問題,循環時間長開銷大和只能保證一個共享變量的原子操做

1.  ABA問題。由於CAS須要在操做值的時候檢查下值有沒有發生變化,若是沒有發生變化則更新,可是若是一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,可是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。

從Java1.5開始JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法做用是首先檢查當前引用是否等於預期引用,而且當前標誌是否等於預期標誌,若是所有相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。

關於ABA問題參考文檔: http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html

2. 循環時間長開銷大。自旋CAS若是長時間不成功,會給CPU帶來很是大的執行開銷。若是JVM能支持處理器提供的pause指令那麼效率會有必定的提高,pause指令有兩個做用,第一它能夠延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它能夠避免在退出循環的時候因內存順序衝突(memory order violation)而引發CPU流水線被清空(CPU pipeline flush),從而提升CPU的執行效率。

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

4、concurrent包的實現

因爲java的CAS同時具備 volatile 讀和volatile寫的內存語義,所以Java線程之間的通訊如今有了下面四種方式:

  1. A線程寫volatile變量,隨後B線程讀這個volatile變量。
  2. A線程寫volatile變量,隨後B線程用CAS更新這個volatile變量。
  3. A線程用CAS更新一個volatile變量,隨後B線程用CAS更新這個volatile變量。
  4. A線程用CAS更新一個volatile變量,隨後B線程讀這個volatile變量。

Java的CAS會使用現代處理器上提供的高效機器級別原子指令,這些原子指令以原子方式對內存執行讀-改-寫操做,這是在多處理器中實現同步的關鍵(從本質上來講,可以支持原子性讀-改-寫指令的計算機器,是順序計算圖靈機的異步等價機器,所以任何現代的多處理器都會去支持某種能對內存執行原子性讀-改-寫操做的原子指令)。同時,volatile變量的讀/寫和CAS能夠實現線程之間的通訊。把這些特性整合在一塊兒,就造成了整個concurrent包得以實現的基石。若是咱們仔細分析concurrent包的源代碼實現,會發現一個通用化的實現模式:

  1. 首先,聲明共享變量爲volatile;
  2. 而後,使用CAS的原子條件更新來實現線程之間的同步;
  3. 同時,配合以volatile的讀/寫和CAS所具備的volatile讀和寫的內存語義來實現線程之間的通訊。

AQS,非阻塞數據結構和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中的基礎類都是使用這種模式來實現的,而concurrent包中的高層類又是依賴於這些基礎類來實現的。從總體來看,concurrent包的實現示意圖以下:

相關文章
相關標籤/搜索