由AtomicInteger開始講CAS

AtomicInteger底層依賴於Unsafe類,基於CAS(compare and swap/set)原理,CAS底層由cpu原語支持,可以保證更新一個Integer變量的原子性。Unsafe類可以直接操作內存,指定在內存的某位置讀取或者寫入某值。

AtomicInteger主要的三個成員變量如下。
U是Unsafe類的一個實例,Unsafe類採用單例模式,其中包含了一些用於操作底層的非安全的方法,雖然這些方法都是public,但是不應該隨便使用,因爲unsafe。
VALUE代表了AtomicInteger.class在jvm的內存分配中,名爲"value"的成員變量在的偏移量,可以利用這個偏移量直接讀取或者更新"value"的值。它是調用Unsafe中的objectFieldOffset方法得到的。
value用volatile修飾,當一個線程修改過之後,其他線程可以立即看到。

private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
private volatile int value;

下面列出一個關鍵代碼,體現其CAS思想。
可以看到AtomicInteger中的getAndIncrement方法,又調用了Unsafe類的getAndAddInt方法。

/**
 * Atomically increments the current value,
 * with memory effects as specified by {@link VarHandle#getAndAdd}.
 *  * <p>Equivalent to {@code getAndAdd(1)}.
 *  * @return the previous value
 */
public final int getAndIncrement() {
    return U.getAndAddInt(this, VALUE, 1);
}

CAS的思想就體現在Unsafe的getAndAddInt中。根據VALUE(value的偏移量offset)直接獲取value在內存中的值v,接下來,調用weakCompareAndSetInt方法嘗試更新,有四個入參:

  • o代表被更新的對象;
  • offset代表成員變量value的偏移量;
  • v代表期望值(上一步通過getIntVolatile(o, offset)查出來的內存中的值),更新的時候,比較該期望值v和內存中該偏移量實時存儲的值是否一致,若一致則更新並返回true,若不一致則不更新並返回false。“若一致則更新”這個步驟是由cpu原語支撐的,是原子的。
  • v+delta代表新值。
/**
 * 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同時更新一個變量時,只有其中一個線程能更新變量的值,其他線程都將失敗。然而失敗的線程並不會被掛起,而是被告知在這次競爭中失敗,並可以再次嘗試。由於一個線程在競爭CAS時失敗不會阻塞,因此它可以決定是否重新嘗試,或者執行一些恢復操作,也可以不執行任何操作。

CAS的典型使用模式是:首先從V中讀取值A,並根據A計算新值B,然後通過CAS以原子方式將V中的值由A變成B(只要在這期間沒有任何線程將V的值修改爲其他值)。由於CAS能檢測到來自其他線程的干擾,因此即使不使用鎖也能夠實現原子的讀-改-寫操作序列。

AtomicInteger保證了變量更新的原子性,而volatile保證了變量的可見性和有序性。那麼我們可以說原子類的更新是線程安全的嗎?No

CAS的缺點:

  • ABA問題。假設value初始值是A,當前線程拿到期望值v=A,在嘗試更新value之前,另一個線程把value更新爲B,另一個線程又把value更新爲A。那麼當前線程在更新value時,發現期望值和內存中的值相同,就進行更新了,而者期間value的值已經被更新過兩次。
  • 自旋消耗。看上面的getAndAddInt方法,在超高併發的場景下,當前線程可能會一直在while循環中,持續佔有時間片,造成資源浪費。
  • 活鎖問題。多個相互協作的線程都對彼此相應從而修改各自的狀態,並使得任何一個線程都無法繼續執行時,就發生了活鎖。就像生活中,兩個有禮貌的人面對面相向而行,都想給對方讓路反而一時之間誰都不能前進半步。

解決方案:

  • ABA問題:
    某些場景下是不需要解決的,例如只是簡單的對一個整數進行加減,不涉及任何業務場景,那麼這個ABA問題就可以忽略,因爲不會對結果產生影響。如果必須要解決,可以記錄對於該對象的更新版本號,每次更新時不僅比較期望值和實際值是否一致,還要比較版本號的期望值和實際值是否一致。每次更新的時候都讓版本號+1。java中的AtomicStampedReference.class和AtomicMarkableReference.class記錄了版本號。
  • 自旋消耗問題:
    破壞掉while死循環,當超過一定時間或者超過一定次數的時候放棄更新,每次重試之前等待一個隨機的時間。
  • 活鎖問題:
    解決活鎖問題,需要在每次重試機制中引入隨機性。每次重新嘗試之前,等待一個隨機的時間。

適用場景

在中低程度的競爭下,atomic類能夠提供更高的可伸縮性,效率更高;而在高強度的競爭下,鎖能夠更有效地避免競爭(使線程阻塞),效率更高。
我們可以通過提高處理競爭的效率來提高可伸縮性,但是隻有完全消除競爭,才能實現真正的可伸縮性。如果能夠避免使用共享狀態,那麼開銷會更加小,例如使用ThreadLocal來保存變量值。

Atomic類,有一個共同特點,只能針對一個變量的更新保持其原子性。若有多個共享變量需要保證併發安全,需要考慮其他方案,當然也可以考慮把這些個變量集成在一個引用類型裏,使用原子類管理。

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