Java原子操作CAS原理解析
一、CAS(Compare And Set)
Compare And Set(或Compare And Swap),CAS是解決多執行緒並行情況下使用鎖造成效能損耗的一種機制,CAS操作包含三個運算元——記憶體位置(V)、預期原值(A)、新值(B)。如果記憶體位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在CAS指令之前返回該位置的值。CAS有效地說明了“我認為位置V應該包含值A;如果包含該值,則將B放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。
在java中可以通過鎖和迴圈CAS的方式來實現原子操作。Java中 java.util.concurrent.atomic包相關類就是 CAS的實現,atomic包裡包括以下類:
AtomicBoolean | 可以用原子方式更新的boolean值。 |
AtomicInteger | 可以用原子方式更新的int值。 |
AtomicIntegerArray | 可以用原子方式更新其元素的int陣列。 |
AtomicIntegerFieldUpdater |
基於反射的實用工具,可以對指定類的指定volatile int欄位進行原子更新。 |
AtomicLong | 可以用原子方式更新的long值。 |
AtomicLongArray | 可以用原子方式更新其元素的long陣列。 |
AtomicLongFieldUpdater |
基於反射的實用工具,可以對指定類的指定volatile long欄位進行原子更新。 |
AtomicMarkableReference |
AtomicMarkableReference維護帶有標記位的物件引用,可以原子方式對其進行更新。 |
AtomicReference |
可以用原子方式更新的物件引用。 |
AtomicReferenceArray |
可以用原子方式更新其元素的物件引用陣列。 |
AtomicReferenceFieldUpdater<T,V> | 基於反射的實用工具,可以對指定類的指定volatile欄位進行原子更新。 |
AtomicStampedReference |
AtomicStampedReference維護帶有整數“標誌”的物件引用,可以用原子方式對其進行更新。 |
二、AtomicInteger
AtomicInteger可以用原子方式更新的 int 值。AtomicInteger 可用在應用程式中(如以原子方式增加的計數器),並且不能用於替換 Integer。但是,此類確實擴充套件了 Number,允許那些處理基於數字類的工具和實用工具進行統一訪問。 我們拿 AtomicInteger為例來學習下 CAS操作是如何實現的。
通常情況下,在 Java中,i++等類似操作並不是執行緒安全的,因為 i++可分為三個獨立的操作:獲取變數當前值,為該值+1,然後寫回新的值。在沒有額外資源可以利用的情況下,只能使用加鎖才能保證讀-改-寫這三個操作時“原子性”的。但是利用加鎖的方式來實現該功能的話,程式碼將非常複雜及難以維護,如:
synchronized (lock) { i++; }
相關類中還需要增加 Object lock等額外標誌,這樣就帶來了很多麻煩,增加了很多業務無關程式碼,給開發與維護帶來了不便。
然而利用 atomic包中相關型別就可以很簡單實現此操作,以下是一個計數程式例項:
public class Counter { private AtomicInteger ai = new AtomicInteger(); private int i = 0; public static void main(String[] args) { final Counter cas = new Counter(); List<Thread> threads = new ArrayList<Thread>(); // 新增100個執行緒 for (int j = 0; j < 100; j++) { threads.add(new Thread(new Runnable() { public void run() { // 執行100次計算,預期結果應該是10000 for (int i = 0; i < 100; i++) { cas.count(); cas.safeCount(); } } })); } //開始執行 for (Thread t : threads) { t.start(); } // 等待所有執行緒執行完成 for (Thread t : threads) { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("非執行緒安全計數結果:"+cas.i); System.out.println("執行緒安全計數結果:"+cas.ai.get()); } /** 使用CAS實現執行緒安全計數器 */ private void safeCount() { for (;;) { int i = ai.get(); // 如果當前值 == 預期值,則以原子方式將該值設定為給定的更新值 boolean suc = ai.compareAndSet(i,++i); if (suc) { break; } } } /** 非執行緒安全計數器 */ private void count() { i++; } } /** 非執行緒安全計數結果:9942 執行緒安全計數結果:10000 */
其中非執行緒安全計數器所計算的結果每次都不相同且不正確,而執行緒安全計數器計算的結果每次都是正確的。
三、存在的問題
CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題:ABA問題、迴圈時間長開銷大、只能保證一個共享變數的原子操作。
ABA問題:因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變數前面追加上版本號,每次變數更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。 從Java1.5開始JDK的 atomic包裡提供了一個類AtomicStampedReference 來解決ABA問題。這個類的 compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設定為給定的更新值。
迴圈時間長開銷大:自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支援處理器提供的pause指令那麼效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出迴圈的時候因記憶體順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。
只能保證一個共享變數的原子操作:當對一個共享變數執行操作時,我們可以使用迴圈CAS的方式來保證原子操作,但是對多個共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變數合併成一個共享變數來操作。比如有兩個共享變數i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用物件之間的原子性,你可以把多個變數放在一個物件裡來進行CAS操作。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。