溫故知新-多線程-深入刨析CAS



摘要

本文從CAS的基本操作開始,逐步探究CAS的實現原理,本文涉及代碼使用JDK1.8版本;

CAS是什麼?

CAS是Compare And Swap (Compare And Exchange) 的簡稱,從因爲的意思也很容易理解:比較並交換。

  • 先看一段代碼,兩個線程分別對atomicInteger加100,因爲AtomicInteger是可以保證++是原子操作的,所以最終輸出結果是:200
public class CasDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                atomicInteger.incrementAndGet();
            }
        },"a").start();
        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                atomicInteger.incrementAndGet();
            }
        },"b").start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info(atomicInteger.get());
    }
}

CAS是如何實現的?

  • AtomicInteger類
    在AtomicInteger數據定義的部分,實際存儲的值是放在value中的,除此之外獲取了unsafe實例,並且定義了valueOffset。再看到static塊,根據加載過程,static塊的加載發生於類加載的時候,是最先初始化的,這時候調用unsafe的objectFieldOffset從Atomic類文件中獲取value的偏移量,那麼valueOffset其實就是記錄value的偏移量的。
public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    private volatile int value;
    ...
}
  • 再看一下incrementAndGet函數
  • var5 = this.getIntVolatile(var1, var2); // 取出Object中偏移地址爲var2的值var5;
  • this.compareAndSwapInt(var1, var2, var5, var5 + var4)比較var1中偏移量爲var2的值是否和var5相等?相等則更新爲var5 + var4;參數換個名字應該會清晰很多:compareAndSwapInt(obj, offset, expect, update);
 /**
 * Atomically increments by one the current value.
 *
 * @return the updated value
 */
public final int incrementAndGet() {
	return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

public final int getAndAddInt(Object var1, long var2, int var4) {
	int var5;
	do {
		// 取出Object中偏移地址爲var2的值var5;
		var5 = this.getIntVolatile(var1, var2);
		// 比較var1中偏移量爲var2的值是否和var5相等?相等則更新爲var5 + var4;
	} while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

	return var5;
}
  • 問題來了?比較並交換就是也是兩個步驟,怎麼能保證線程同步呢?
    下載一下Hotspot源碼,看到compareAndSwapInt實現的源碼如圖所示,發現最終調用了Atomic::cmpxchg(x, addr, e)方法。

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
JNI

翻看源碼(如下面兩張圖所示)可以看到,不同的平臺有不同的實現方式;

  • 在x86的架構下實現,是通過LOCK_IF_MP加鎖的方式實現
  • os::is_MP判斷當前系統是否爲多核系統,如果是就給總線加鎖,所以同一芯片上的其他處理器就暫時不能通過總線訪問內存,保證了該指令在多處理器環境下的原子性。
  • __asm__說明是ASM彙編,__volatile__禁止編譯器優化,
// Adding a lock prefix to an instruction on MP machine
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

cmpxchg
在atomic.cpp中則是通過遞歸實現的;

因爲根據IA64手冊,X86_64架構下,不跨越cacheline的8byte讀寫是原子的,如果你有個指針,沒有跨越cacheline,那麼多線程對這個指針的複製和讀取都是不需要加鎖的,可以保證原子的讀到這8byte;

在這裏插入圖片描述

CAS存在的問題?

  • ABA的問題
    CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。這就是CAS的ABA問題。
    常見的解決思路是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。
    目前在JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。

  • 循環時間長開銷大
    如果CAS不成功,則會原地自旋,如果長時間自旋會給CPU帶來非常大的執行開銷。


你的鼓勵也是我創作的動力

打賞地址

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