CAS 竟然能夠代替 synchorinzed

學習過多線程的同窗必定看到過 CAS 這個概念,CASCompare-and-swap 的簡稱,那它有什麼做用呢 ?爲何可以代替 synchorinzed?java

CAS

CAS(Compare-and-swap):比較和替換,它是設計併發算法是的一種經常使用技術。使用 CAS 操做可用於保證變量更新的原子性。算法

CAS 操做涉及到 3 個值:安全

  • V 當前值:變量當前在內存中的值
  • A 指望值:指望變量當前在內存中的值
  • B 更新值:準備爲變量賦予的新值

CAS 操做邏輯以下:CAS 比較 VA 的值,若是值相等則變量值更新爲 B,不然不進行任何操做。以下圖:markdown

image

若是第一次接觸到 CAS 的概念可能對它的操做邏輯產生疑惑,爲何在相等的狀況下執行賦值操做而不等的狀況下反而什麼都不作呢?下面看一個自增操做的示例:多線程

/**
 * Atomically increments by one the current value.
 *
 * @return the updated value
 */
public final int incrementAndGet() {
    for (;;) {
        //獲取當前值, 此示例中當前值也是本次 `CAS` 的指望值
        int current = get();
        //獲取更新值
        int next = current + 1;
        //執行CAS操做
        if (compareAndSet(current, next)) {
            //成功後纔會返回指望值,不然無線循環
            return next;
        }
    }
}
複製代碼

如今有線程 A、B,當線程 A 執行到 CAS 操做, 獲取當前值、指望值和更新值分別爲 0、0、1, 此時線程 A 被掛起,線程 B 進入執行 CAS 操做將變量值成功更新爲 1, 線程 A 繼續執行 CAS 操做, 因爲此時變量當前值已經被修改,因此本次 CAS 執行失敗,循環繼續執行 CAS 自增操做,執行成功退出循環。併發

CAS VS synchorinzed

經過上面的示例知道 CAS 能夠保證變量更新的原子性,進而能夠聯想到 volatile 關鍵字的功能缺陷。ide

先來看 volatile 關鍵字的做用,以下:高併發

  • 有序性:防止重排序;
  • 可見性:變量更新時全部線程均可以訪問到變量的最新值;
  • 原子性:只能保證單次讀、寫操做的原子性。

volatile 關鍵字的缺陷正是其沒法保證變量操做的原子性,好比單目運算符 ++、-- 就涉及到讀寫兩個操做。因此常常能夠看到 volatilesynchorinzed 關鍵字共用的場景以保證變量操做的原子性,而 CAS 也能夠保證變量操做的原子性。那 CAS 是否能夠替代 synchorinzed 呢?在某些狀況下是能夠的。性能

好比在上面的示例中,AtomicInteger().getAndIncrement() 的內部源碼以下:學習

/**
 * Atomically increments by one the current value.
 *
 * @return the previous value
 */
public final int getAndIncrement() {
    return U.getAndAddInt(this, VALUE, 1);
}

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}
複製代碼

能夠看到這裏就是經過 volatile + CAS 的操做來保證變量操做安全性的。

那下面對 CASsynchorinzed 的使用作下對比,主要從以下兩方面:

功能限制:

  • CAS 更加輕量級,synchorinzed 升級爲重量鎖時會影響系統性能;
  • CAS 僅能保證單個變量操做的原子性,synchorinzed 能夠保證代碼塊內全部變量操做的原子性。

併發規模:

  • 低併發:CAS 更具優點,synchorinzed 在少許狀況下仍可能升級爲重量鎖影響系統性能。
  • 高併發:synchorinzed 更具優點,因爲 CAS 的不少實現都會使用了自旋操做(以下文將介紹的 Atomic*** 系列),當在大量線程的狀況下 CAS 會頻繁執行失敗進而須要頻繁重試,這樣會浪費 CPU 資源。

結論: 在少許線程且僅需保證單個變量線程安全的狀況下可以使用 volatile + CAS 替代 volatile + synchorinzed

注意:volatile + CASvolatile + synchorinzed 使用時要理解它們各自的角色和起到的做用:

  • volatile:保證有序性和可見性;
  • CASsynchorinzed:保證操做原子性。

注意:多線程環境下正確使用 CAS 必須搭配 volatile 關鍵字。由於 CAS 雖然能夠保證原子性,但其沒法保證變量在不一樣線程內存空間的安全性,因此須要 volatile 來保證變量更新對於不一樣線程是可見的。

ABA 問題

除了上文提到過 CAS 的兩個缺點:

  • 僅能保證單個變量操做的原子性;
  • 在高併發狀況下 CAS 一直失敗會一直重試,浪費 CPU 資源針。對這個問題的一個思路是引入退出機制,如重試次數超過必定閾值後失敗退出。固然,更重要的是避免在高併發環境下使用 CAS

還有一個問題就是 ABA 問題,現有線程 A、B

  1. 線程 1 讀取內存中的數據爲 A
  2. 線程 2 修改內存數據爲 B
  3. 線程 2 修改內存數據爲 A
  4. 線程 1 對數據執行 CAS 操做。

因爲執行到第四步時內存數據仍然爲 A,但其實數據已經被修改過了。這就是 ABA 問題。針對 ABA 問題能夠經過引入版本號的方式解決,每次修改內存中的值版本號都 +1,在執行 CAS 操做是不只比較內存中的值也比較版本號,只有二者都相同時才執行成功。Java 中提供的 java.util.concurrent.atomic.AtomicStampedReference 也是經過版本號來解決 ABA 問題的。

在 Android 中的使用

Android 中咱們也能夠經過 Atomic*** 類來使用 volatile + CAS

  • AtomicFile
  • AtomicInteger
  • AtomicLong
  • AtomicBoolean
  • AtomicReferenceFieldUpdater
  • AtomicStampedReference

前幾個比較好理解,分別能夠保證 file、int、long、boolean 類型數據的原子操做,那麼若是操做數據爲 String 或類型不可知怎麼辦呢?這時候就可使用 AtomicReferenceFieldUpdater() 了。在 Kotlin.lazy 的實現中就使用到了 AtomicReferenceFieldUpdater

private class SafePublicationLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable {
    @Volatile private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // this final field is required to enable safe initialization of the constructed instance
    private val final: Any = UNINITIALIZED_VALUE

    override val value: T
        get() {
            val value = _value
            if (value !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return value as T
            }

            val initializerValue = initializer
            // if we see null in initializer here, it means that the value is already set by another thread
            if (initializerValue != null) {
                val newValue = initializerValue()
                if (valueUpdater.compareAndSet(this, UNINITIALIZED_VALUE, newValue)) {
                    initializer = null
                    return newValue
                }
            }
            @Suppress("UNCHECKED_CAST")
            return _value as T
        }

    ......
    
    companion object {
        private val valueUpdater = java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater(
            SafePublicationLazyImpl::class.java,
            Any::class.java,
            "_value"
        )
    }
}
複製代碼

AtomicReferenceFieldUpdater 經過靜態方法 newUpdater() 獲取實例對象。newUpdater() 方法有三個參數,分別是:

  • tclass:目標變量所在類的 class 對象;
  • vclass:目標變量的類型 class 對象;
  • fieldName:目標變量名。

在上述 kotlin.lazy 源碼中經過比較初始值,保證在多線程環境中僅第一次賦值有效:

valueUpdater.compareAndSet(this, UNINITIALIZED_VALUE, newValue)
複製代碼

再來看 AtomicStampedReference,它的初始化方法以下:

public AtomicStampedReference(V initialRef, int initialStamp) {
}
複製代碼

能夠看到初始化方法須要兩個參數:

  • 初始值
  • 暫且看作初始版本號

用法以下:

private val atomicObj: AtomicStampedReference<String> = AtomicStampedReference("A", 0)

val t1 = Thread {
    try {
        TimeUnit.SECONDS.sleep(1)
    } catch (e: InterruptedException) {
    }
    println("run thread1")
    atomicObj.compareAndSet("A", "B", atomicObj.stamp, atomicObj.stamp + 1)
    atomicObj.compareAndSet("B", "A", atomicObj.stamp, atomicObj.stamp + 1)
}

val t2 = Thread {
    val stamp: Int = atomicObj.stamp
    try {
        TimeUnit.SECONDS.sleep(2)
    } catch (e: InterruptedException) {
    }
    println("run thread2")
    val result: Boolean = atomicObj.compareAndSet("A", "B", stamp, stamp + 1)
    println(result) // false
}

t1.start()
t2.start()
複製代碼

能夠看到因爲 Thread2 提早獲取了版本號,及時 Thread1 執行以後值依然是 A, 但因爲版本號已然發生了變化因此 Thread2 的執行結果仍然是 false

相關文章
相關標籤/搜索