科普:CAS 和 ABA

CAS簡介

CAS 全稱是 compare and swap,是一種用於在多線程環境下實現同步功能的機制。

CAS 它是一條CPU併發原語。操作包含三個操作數 -- 內存位置、預期數值和新值。CAS 的實現邏輯是將內存位置處的數值與預期數值想比較,若相等,則將內存位置處的值替換為新值。若不相等,則不做任何操作。這個過程是原子的。

CAS併發原語體現在java語言中的sun.misc.Unsafe類中的各個方法。調用Unsafe類中的CAS方法,JVM會幫我們實現彙編指令。這是一種完全依賴硬件的功能,通過它實現了原子操作。由於CAS是一種系統原語,原語屬於操作系統用語範疇,是由若干條指令組成的,用於完成某個功能的一個過程,並且原語的執行必須是連續的,在執行過程中不允許被打斷,也就是說CAS是一條CPU的原子指令,不會造成所謂的數據不一致問題。

Unsafe類

Unsafe類是CAS的核心類,由於Java方法無法直接訪問底層系統,需要通過本地(native)方法來訪問,基於該類可以直接操作特定內存的數據。Unsafe類存在與sum.misc包中,其內部方法操作可以像C的指針一樣直接操作內存,因為Java中CAS操作的執行依賴於Unsafe類的方法。

Unsafe類中的所有方法都是native修飾的,也就是說Unsafe類中的方法都直接調用操作系統底層資源執行相應任務。

代碼解析

<code>public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);

// 運行結果: true 2019
System.out.println(atomicInteger.compareAndSet(5, 2019) + "\\t" + atomicInteger.get());
// 運行結果: false 2019
System.out.println(atomicInteger.compareAndSet(5, 1024) + "\\t" + atomicInteger.get());

// 此方法可以解決多線程環境下i++問題,底層使用的是Unsafe類CAS和自旋鎖
atomicInteger.getAndIncrement();
}
}/<code>

源碼分析:

<code>/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
// this=當前對象 valueOffset=內存偏移量(內存地址) 1=固定值,每次調用+1
// Unsafe就是根據內存偏移地址獲取數據的。
return unsafe.getAndAddInt(this, valueOffset, 1);
}

/**
* 為了方便查看和添加註釋,此方法是從Unsafe類中複製出來的
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 獲取var1對象,內存地址在var2的值。

// 相當於這個線程從主物理內存中取值copy到自己的工作內存中。
var5 = this.getIntVolatile(var1, var2);

// 比較並交換,如果var1對象,內存地址在var2的值和var5值一樣,那麼就+1
// compareAndSwapInt如果返回true,取反為false,說明更新成功,退出循環,則返回。
// compareAndSwapInt如果返回false,取反為true,說明當前線程工作內存中的值和主物理內存中的值不一樣,被其他線程修改了,則繼續循環獲取比較,直到更新成功為止。
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}
/<code>

執行過程說明:

  • 假設線程A和線程B兩個線程同時執行 getAndAddInt操作(分別跑在不同CPU上):
  • AtomicInteger裡面的value原始值為3,即主內存中 AtomicInteger的value為3,根據JMM模型,線程A和線程B各自持有一
    份值為3的value的副本分別到各自的工作內存。
  • 線程A通過 getIntVolatile(var1,var2)拿到value值3,這時線程A被掛起。
  • 線程B也通過 getIntVolatile(var1,var2)方法獲取到value值3,此時剛好線程B沒有被掛起並執行 compareAndSwapInt方法

    比較內存值也為3,成功修改內存值為4,線程B改完收工,一切OK。
  • 這時線程A恢復,執行 compareAndSwapInt方法比較,發現自己手裡的值數字3和主內存的值數字4不一致,說明該值已
    經被其它線程搶先一步修改過了,那A線程本次修改失敗,只能重新讀取重新來一遍了。
  • 線程A重新獲取 value值,因為變量value被 volatile修飾,所以其它線程對它的修改,線程A總是能夠看到,線程A繼續執
    了 compareAndSwapInt進行比較替換,直到成功。

volatile簡單說明:volatile是一個輕量級的同步機制, 三大特性: 保證可見性, 不保證原子性, 禁止指令重排。

  • 可見性: 多個線程從主內存中copy一份數據,修改後,需要將自己的數據重新寫入主內存,並通知其他線程數據已更新,保證數據可見性,和多線程數據一致性。
  • 禁止指令重排: 由於指令重排,會對代碼的執行順序進行優化,可能會導致最後的結果和期望的結果不一致,所以需要禁止重排。

CAS的優缺點

優點

  • 不需要加鎖,保持了一致性和併發性。

缺點

  • 循環時間長開銷很大:我們可以看到getAndAddInt方法執行時,如果CAS失敗,會一直進行嘗試。如果CAS長時間一直不成功,可能會給CPU帶來很大的開銷。
  • 只能保證一個共享變量的原子操作:當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖來保證原子性。
  • ABA問題:下面會提供詳細案例

ABA問題

舉個栗子說明:主內存有個數據值:A,兩個線程A和B分別copy主內存數據到自己的工作區,A執行比較慢,需要10秒, B執行比較快,需要2秒, 此時B線程將主內存中的數據更改為B,過了一會又更改為A,然後A線程執行比較,發現結果是A,以為別人沒有動過,然後執行更改操作。其實中間已經被更改過了,這就是ABA問題。也就是ABA問題只要開始時的數據和結束時的數據一致,我就認為沒改過,不管過程。儘管A線程的CAS操作是成功的,但是不代表這個過程就是沒問題的。

ABA問題說簡單點就是,預判值還是和當初抓取的一樣,但是這個“ 值 ”的版本可能不一樣了,在某些不僅要考慮數據值是否一致,還要考慮版本是否一致的場景下需要注意.

Java併發包為了解決這個問題,提供了一個帶有標記的原子引用類“AtomicStampedReference”,它可以通過控制變量值的版本來保證CAS的正確性。

解決ABA問題的代碼示例

<code>/**
* 解決CAS的ABA問題
*/
public class SolveABAOfCAS {

static AtomicReference<integer> atomicReference = new AtomicReference<>(100);
static AtomicStampedReference<integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);

public static void main(String[] args) throws InterruptedException {
System.out.println("==========以下是ABA問題的產生==========");
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start();

new Thread(() -> {
try {
// 暫停1秒鐘,保證上面完成一次ABA操作
Thread.sleep(1000);
System.out.println(atomicReference.compareAndSet(100, 2019) + "\\t" + atomicReference.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();

Thread.sleep(2000);
System.out.println("==========以下是ABA問題的解決==========");

new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\\t第1次版本號" + stamp);
try {
// 暫停一秒鐘t3線程
Thread.sleep(1000);
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\\t第2次版本號" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\\t第3次版本號" + atomicStampedReference.getStamp());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t3").start();

new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\\t第1次版本號" + stamp);
try {
// 暫停3秒鐘t4線程,保證上面的t3線程完成一次ABA操作
Thread.sleep(3000);
boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\\t修改成功否: " + result + "\\t當前最新實際版本號: " + atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName() + "\\t當前實際最新值: " + atomicStampedReference.getReference());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t4").start();

}
}
/<integer>/<integer>/<code>


科普:CAS 和 ABA


原文:http://jianjieming.com/index.php/archives/101/


分享到:


相關文章: