Java併發編程系列-(3) 原子操作與CAS

3. 原子操作與CAS

3.1 原子操作

所謂原子操作是指不會被線程調度機制打斷的操作;這種操作一旦開始,就一直運行到結束,中間不會有任何context switch,也就是切換到另一個線程。

爲了實現原子操作,Java中可以通過synchronized關鍵字將函數或者代碼塊包圍,以實現操作的原子性。但是synchronized關鍵字有一些很顯著的問題:

1、synchronized是基於阻塞鎖的機制,如果被阻塞的線程優先級很高,可能很長時間其他線程都沒有機會運行;
2、拿到鎖的線程一直不釋放鎖,可能導致其他線程一直等待;
3、線程數量很多時,可能帶來大量的競爭,消耗cpu,同時帶來死鎖或者其他安全。

像synchronized這種獨佔鎖屬於悲觀鎖,它是在假設一定會發生衝突的,那麼加鎖恰好有用,除此之外,還有樂觀鎖,樂觀鎖的含義就是假設沒有發生衝突,那麼我正好可以進行某項操作,如果要是發生衝突呢,那我就重試直到成功,樂觀鎖最常見的就是CAS。

JAVA內部在實現原子操作的類時都應用到了CAS。

3.2 CAS

CAS是CompareAndSwap的縮寫,即比較並替換。CAS需要有3個操作數:內存地址V,舊的預期值A,即將要更新的目標值B。

CAS指令執行時,當且僅當內存地址V的值與預期值A相等時,將內存地址V的值修改爲B,否則就什麼都不做。整個比較並替換的操作是一個原子操作,大多數現代處理器都支持CAS指令。

JDK中關於原子操作的類有,

  • 更新基本類型類:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

  • 更新數組類:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

  • 更新引用類型:AtomicReference,AtomicMarkableReference,AtomicStampedReference

  • 原子更新字段類: AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater

下面例子爲AtomicInteger的基本用法,

public class UseAtomicInt {
    
    static AtomicInteger ai = new AtomicInteger(10);
    
    public static void main(String[] args) {
        System.out.println(ai.getAndIncrement());//10--->11
        System.out.println(ai.incrementAndGet());//11--->12--->out
        System.out.println(ai.get());
    }
}

深入getAndIncrement的實現可以發現,內部調用了CAS進行value的更新,

   /**
     * Atomically adds the given value to the current value of a field
     * or array element within the given object {@code o}
     * at the given {@code offset}.
     *
     * @param o object/array to update the field/element in
     * @param offset field/element offset
     * @param delta the value to add
     * @return the previous value
     * @since 1.8
     */
    @HotSpotIntrinsicCandidate
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
        return v;
    }

CAS在底層通過JNI調用系統的Atomic::cmpxchg方法,來進行value的比較與更新。

對於CAS的實現,這依賴於CPU提供的特定指令,具體根據體系結構的不同還存在着明顯區別。比如,x86 CPU 提供 cmpxchg 指令;而在精簡指令集的體系架構中,則通常是靠一對兒指令(如“load and reserve”和“store conditional”)實現的,在大多數處理器上 CAS 都是個非常輕量級的操作,這也是其優勢所在。

3.3 CAS的問題

CAS雖然很高效的解決了原子操作問題,但是CAS仍然存在三大問題。

  • ABA問題

什麼是ABA問題?ABA問題怎麼解決?如果內存地址V初次讀取的值是A,並且在準備賦值的時候檢查到它的值仍然爲A,那我們就能說它的值沒有被其他線程改變過了嗎?如果在這段期間它的值曾經被改成了B,後來又被改回爲A,那CAS操作就會誤認爲它從來沒有被改變過。這個漏洞稱爲CAS操作的“ABA”問題。
Java併發包爲了解決這個問題,提供了一個帶有標記的原子引用類“AtomicStampedReference”,它可以通過控制變量值的版本來保證CAS的正確性。因此,在使用CAS前要考慮清楚“ABA”問題是否會影響程序併發的正確性,如果需要解決ABA問題,改用傳統的互斥同步可能會比原子類更高效。

  • 開銷問題

循環時間長開銷很大:我們可以看到getAndAddInt方法執行時,如果CAS失敗,會一直進行嘗試。如果CAS長時間一直不成功,可能會給CPU帶來很大的開銷。

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

只能保證一個共享變量的原子操作:當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖來保證原子性。


本文由『後端精進之路』原創,首發於博客 http://teckee.github.io/ , 轉載請註明出處

搜索『後端精進之路』關注公衆號,立刻獲取最新文章和價值2000元的BATJ精品面試課程

後端精進之路.png

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