CAS操作确保原子性
(一)CAS操作
在JDK 5之前Java語言是靠synchronized關鍵字保證同步的,這會導致有鎖
鎖機制存在以下問題:
(1)在多線程競爭下,加鎖、釋放鎖會導致比較多的上下文切換和調度延時,引起性能問題。
(2)一個線程持有鎖會導致其它所有需要此鎖的線程掛起。
(3)如果一個優先級高的線程等待一個優先級低的線程釋放鎖會導致優先級倒置,引起性能風險。
volatile是不錯的機制,但是volatile不能保證原子性。因此對于同步最終還是要回到鎖機制上來。
獨占鎖是一種悲觀鎖,synchronized就是一種獨占鎖,會導致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。而另一個更加有效的鎖就是樂觀鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。樂觀鎖用到的機制就是CAS,Compare and Swap。
一、什么是CAS
CAS,compare and swap的縮寫,中文翻譯成比較并交換。
我們都知道,在java語言之前,并發就已經廣泛存在并在服務器領域得到了大量的應用。所以硬件廠商老早就在芯片中加入了大量直至并發操作的原語,從而在硬件層面提升效率。在intel的CPU中,使用cmpxchg指令。
在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 會檢測它(并失敗),算法 可以對該操作重新計算。
二、CAS的目的
利用CPU的CAS指令,同時借助JNI來完成Java的非阻塞算法。其它原子操作都是利用類似的特性完成的。而整個J.U.C都是建立在CAS之上的,因此對于synchronized阻塞算法,J.U.C在性能上有了很大的提升。
三、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操作。
四、 concurrent包的實現
由于java的CAS同時具有 volatile 讀和volatile寫的內存語義,因此Java線程之間的通信現在有了下面四種方式:
Java的CAS會使用現代處理器上提供的高效機器級別原子指令,這些原子指令以原子方式對內存執行讀-改-寫操作,這是在多處理器中實現同步的關鍵(從本質上來說,能夠支持原子性讀-改-寫指令的計算機器,是順序計算圖靈機的異步等價機器,因此任何現代的多處理器都會去支持某種能對內存執行原子性讀-改-寫操作的原子指令)。同時,volatile變量的讀/寫和CAS可以實現線程之間的通信。把這些特性整合在一起,就形成了整個concurrent包得以實現的基石。如果我們仔細分析concurrent包的源代碼實現,會發現一個通用化的實現模式:
AQS,非阻塞數據結構和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中的基礎類都是使用這種模式來實現的,而concurrent包中的高層類又是依賴于這些基礎類來實現的。從整體來看,concurrent包的實現示意圖如下:
(二)在AtomicInteger中應用
CAS原理:?
? ? ? 通過查看AtomicInteger的源碼可知,?
? ? ? ?`private volatile int value;
public final boolean compareAndSet(int expect, int update) {?
? ? ? ? ? ? ? ? ? ? return unsafe.compareAndSwapInt(this, valueOffset, expect, update);?
? ? ? ? ? ? ? ?} `?
? ? ? ? ? ? 通過申明一個volatile (內存鎖定,同一時刻只有一個線程可以修改內存值)類型的變量,再加上unsafe.compareAndSwapInt的方法,來保證實現線程同步的。
二、CAS(Compare and Swap)
CAS指令在Intel CPU上稱為CMPXCHG指令,它的作用是將指定內存地址的內容與所給的某個值相比,如果相等,則將其內容替換為指令中提供的新值,如果不相等,則更新失敗。這一比較并交換的操作是原子的,不可以被中斷。初一看,CAS也包含了讀取、比較 (這也是種操作)和寫入這三個操作,和之前的i++并沒有太大區別,是的,的確在操作上沒有區別,但CAS是通過硬件命令保證了原子性,而i++沒有,且硬件級別的原子性比i++這樣高級語言的軟件級別的運行速度要快地多。雖然CAS也包含了多個操作,但其的運算是固定的(就是個比較),這樣的鎖定性能開銷很小。
從內存領域來說這是樂觀鎖,因為它在對共享變量更新之前會先比較當前值是否與更新前的值一致,如果是,則更新,如果不是,則無限循環執行(稱為自旋),直到當前值與更新前的值一致為止,才執行更新。
??簡單的來說,CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改為B,否則返回V。這是一種樂觀鎖的思路,它相信在它修改之前,沒有其它線程去修改它;而Synchronized是一種悲觀鎖,它認為在它修改之前,一定會有其它線程去修改它,悲觀鎖效率很低。下面來看一下AtomicInteger是如何利用CAS實現原子性操作的。
volatile變量
[java]?view plaincopy[java]?view plaincopy
Compare And Set
[java]?view plaincopy比較并設置,這里利用Unsafe類的JNI方法實現,使用CAS指令,可以保證讀-改-寫是一個原子操作。compareAndSwapInt有4個參數,this - 當前AtomicInteger對象,Offset - value屬性在內存中的位置(需要強調的是不是value值在內存中的位置),expect - 預期值,update - 新值,根據上面的CAS操作過程,當內存中的value值等于expect值時,則將內存中的value值更新為update值,并返回true,否則返回false。在這里我們有必要對Unsafe有一個簡單點的認識,從名字上來看,不安全,確實,這個類是用于執行低級別的、不安全操作的方法集合,這個類中的方法大部分是對內存的直接操作,所以不安全,但當我們使用反射、并發包時,都間接的用到了Unsafe。
循環設置
現在在來看開篇提到的兩個方法,我們拿incrementAndGet來分析一下其實現過程。 [java]?view plaincopy循環內,獲取當前值并設置更新值,調用compareAndSet進行CAS操作,如果成功就返回更新至,否則重試到成功為止。這里可能存在一個隱患,那就是循環時間過長,總是在當前線程compareAndSet時,有另一個線程設置了value(點子太背了),這個當然是屬于小概率時間,目前Java貌似還不能處理這種情況。
CAS用作原子操作
現在CPU內部已經執行原子的CAS操作。Java5以來,你可以使用java.util.concurrent.atomic包中的一些原子類來使用CPU中的這些功能。
下面是一個使用AtomicBoolean類實現lock()方法的例子:
[java]?view plaincopylocked變量不再是boolean類型而是AtomicBoolean。這個類中有一個compareAndSet()方法,它使用一個期望值和AtomicBoolean實例的值比較,和兩者相等,則使用一個新值替換原來的值。在這個例子中,它比較locked的值和false,如果locked的值為false,則把修改為true。
如果值被替換了,compareAndSet()返回true,否則,返回false。
使用Java5+提供的CAS特性而不是使用自己實現的的好處是Java5+中內置的CAS特性可以讓你利用底層的你的程序所運行機器的CPU的CAS特性。這會使還有CAS的代碼運行更快。
簡單例子:
[java]?view plaincopycas缺點
雖然使用CAS可以實現非阻塞式的原子性操作,但是會產生ABA問題,關于ABA問題:? ? ? ?有ABA問題(即在更新前的值是A,但在操作過程中被其他線程更新為B,又更新為 A),這時當前線程認為是可以執行的,其實是發生了不一致現象,如果這種不一致對程序有影響(真正有這種影響的場景很少,除非是在變量操作過程中以此變量為標識位做一些其他的事,比如初始化配置),則需要使用AtomicStampedReference(除了對更新前的原值進行比較,也需要用更新前的 stamp標志位來進行比較)。
總結:?
可以用CAS在無鎖的情況下實現原子操作,但要明確應用場合,非常簡單的操作且又不想引入鎖可以考慮使用CAS操作,當想要非阻塞地完成某一操作也可以考慮CAS。不推薦在復雜操作中引入CAS,會使程序可讀性變差,且難以測試,同時會出現ABA問題
總結
以上是生活随笔為你收集整理的CAS操作确保原子性的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据预处理—数据清洗(2)—异常值(极值
- 下一篇: 音频隐写术:分析剑桥大学提出的MP3St