關於CAS的一點理解和思考

CAS

CAS(Compare And Swap)是對一種處理器指令的稱呼,中文譯爲:比較並交換。
它需要三個參數:內存地址V、期望的舊值A、要替換的新值B。
它要完成的功能:當且僅當內存地址V的值等於A時,將A替換爲B並返回true,否則什麼也不做直接返回false。
用Java代碼描述,大致如下所示:

/**
 * @paramv 內存地址
 * @param a 期望舊值
 * @param b 替換的新值
 * @return
 */
boolean compareAndSwap(V v,A a,B b){
	if (v == a){
		v = b;
		return true;
	}
	return false;
}

可以看到CAS是一個典型的check-and-act操作,如果不加鎖很明顯它不是一個原子操作。

JUC下很多類都用到了大量的CAS操作,如:AQS、ConcurrentHashMap、atomic包下的原子變量類。
它們是如何做到在不加鎖的情況下確保多線程安全的呢?


Java中的CAS操作

CAS操作依賴於現代CPU支持的併發原語,換句話說,整個“比較並交換”的過程CPU會保證它的原子性,執行過程中不會因爲時間片用完而被中斷。
因此Java語言本身是無法實現CAS操作的,需要藉助JNI調用本地代碼來實現。

在Java平臺中,利用sun.misc.Unsafe類來完成CAS操作,查看源碼會發現大多數方法都是被native修飾的,意味這Java需要調用其他語言的代碼才能實現CAS操作。

正如它的名字一樣,Unsafe是一個不安全的類,使用它可以直接操作內存地址、分配堆外內存。使用Unsafe分配的內存GC是不會自動回收的,因此一旦使用不當很容易造成內存泄漏,所以JDK對Unsafe類的使用做了一些限制。

Unsafe構造方法被私有化了,不能直接new實例,提供了一個獲取實例的方法:getUnsafe(),源碼如下:

@CallerSensitive
public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}

public static boolean isSystemDomainLoader(ClassLoader var0) {
   return var0 == null;
}

要想拿到Unsafe實例,首先會檢查調用類的類加載器是否爲null,否則會拋出異常。
只有被Bootstrap ClassLoader類加載器加載的類才符合這個條件,意味着開發者自己編寫的類是無法獲取Unsafe實例的,因爲Java壓根就沒打算讓開發者去用它。

雖然不能通過正規渠道去使用Unsafe,但是可以藉助Java的反射機制來獲取。

  • 線程不安全的int自增示例:
public class UnsafeIncr {
	volatile int i = 0;//volatile能保證可見性,但是無法保證i++的原子性

	void incr(){
		i++;
	}

	public static void main(String[] args) throws InterruptedException {
		UnsafeIncr incr = new UnsafeIncr();
		//10線程 每個線程自增1萬次 期望結果10萬
		for (int i = 0; i < 10; i++) {
			new Thread(()->{
				for (int j = 0; j < 10000; j++) {
					incr.incr();
				}
			}).start();
		}

		//主線程休眠1s,等待10個線程執行結束
		Thread.sleep(1000);
		System.out.println(incr.i);
		//輸出結果幾乎總是小於10萬
	}
}
  • 利用CAS實現的線程安全的int自增:
public class CASIncr {
	static final Unsafe unsafe;
	static final long fieldOffset;
	public int index = 0;

	static {
		try {
			//反射獲取Unsafe實例
			Field field = Unsafe.class.getDeclaredField("theUnsafe");
			field.setAccessible(true);
			unsafe = (Unsafe) field.get(null);
			//計算 index屬性相對於UnsafeDemo類的內存偏移量
			fieldOffset = unsafe.objectFieldOffset(CASIncr.class.getField("index"));
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage());
		}
	}

	//index利用CAS完成自增操作
	void incr(){
		int old;
		do {
			//獲取舊值
			old = unsafe.getIntVolatile(this, fieldOffset);
			//CAS操作 如果內存地址值=old,說明沒有被其他線程修改,將新值替換爲old+1並返回true,否則返回false再次自旋
		} while (!unsafe.compareAndSwapInt(this, fieldOffset, old, old + 1));
	}
}

利用CAS實現的自增操作是線程安全的,輸出結果總是正確的。


加鎖和CAS

加鎖是保證線程安全功能最強大,也是適用範圍最廣的一種解決方案。
但是僅對於簡單變量的讀寫操作來加鎖,未免顯得有點“殺雞焉用牛刀”。

CAS可以看成是一種樂觀鎖的實現機制,假定不存在多線程競爭,如果變量沒有被其他線程修改過那麼直接寫入成功,否則自旋進行重試,直到成功爲止。

加鎖(不考慮鎖膨脹,指重量級鎖)是一種悲觀鎖機制,認爲肯定會發生數據衝突,通過OS級別的互斥量來保證每次最多隻有一個線程進入臨界區,通過掛起和喚醒線程來保證數據安全。

在單線程下,CAS的效率肯定是最高的,由於沒有線程競爭,每次寫入都會成功,完全不需要重試。
但是在線程競爭比較激烈的情況下,需要進行多次重試才能寫入成功,反而會浪費CPU的性能。

這也就是爲什麼JDK6中加入“自適應自選鎖”的原因,競爭不激烈CAS自旋重試比掛起再喚醒線程的效率高,競爭激烈就直接掛起線程,避免浪費CPU的性能。

性能測試比較

int index = 0,自增一億次,分別使用synchronized和CAS測試,對比耗時:

public class SyncTest {
	private int index = 0;
	private final int count = 100000000;
	private long startTime = System.currentTimeMillis();

	synchronized void incr(){
		index++;
		if (index == count) {
			System.out.println(System.currentTimeMillis() - startTime);
			System.exit(1);
		}
	}
}

public class CASTest {
	//不用JDK提供的原子類,自己實現
	static final Unsafe unsafe;
	static final long fieldOffset;
	public int index = 0;
	private final int count = 100000000;
	private long startTime = System.currentTimeMillis();

	static {
		try {
			Field field = Unsafe.class.getDeclaredField("theUnsafe");
			field.setAccessible(true);
			unsafe = (Unsafe) field.get(null);
			fieldOffset = unsafe.objectFieldOffset(CASTest.class.getField("index"));
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage());
		}
	}

	//直接使用unsafe.getAndAddInt()性能更好,這裏爲了更好的理解CAS,故採用compareAndSwapInt()
	void incr(){
		int oldValue;
		do {
			//獲取當前值
			oldValue = unsafe.getIntVolatile(this, fieldOffset);

			//如果內存地址V的值=oldValue,則替換爲(oldValue+1)並返回true,否則什麼也不做直接返回false(說明值已被其他線程修改),繼續自旋。
			//依賴於現代CPU提供的併發原語,CPU會保證整個比較並交換的動作的原子性。
		} while (!unsafe.compareAndSwapInt(this, fieldOffset, oldValue, oldValue + 1));

		if (oldValue == count - 1) {
			System.out.println(System.currentTimeMillis() - startTime);
			System.exit(1);
		}
	}
}
線程數 Sync耗時(ms) CAS耗時(ms)
1 2456 1092
2 4183 5201
5 5633 8785

CAS不同線程數量下的額外開銷

int index = 0,自增一億次,分別測試CAS在不同數量線程的競爭下額外的自旋次數。

public class CAS {
	static final Unsafe unsafe;
	static final long fieldOffset;
	public int index = 0;
	private int count = 100000000;
	private long startTime = System.currentTimeMillis();//開始時間
	private AtomicInteger casCount = new AtomicInteger(0);//統計CAS次數的原子類

	static {
		try {
			//反射拿到Unsafe實例、獲取index屬性的偏移量
			Field field = Unsafe.class.getDeclaredField("theUnsafe");
			field.setAccessible(true);
			unsafe = (Unsafe) field.get(null);
			fieldOffset = unsafe.objectFieldOffset(CASTest.class.getField("index"));
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage());
		}
	}

	void incr(){
		while (true) {
			int old = unsafe.getIntVolatile(this, fieldOffset);
			//CAS自增成功,結束自旋
			if (unsafe.compareAndSwapInt(this, fieldOffset, old, old + 1)) {
				break;
			}else {
				//統計額外的自旋次數
				casCount.incrementAndGet();
			}
		}

		//自增一億次結束,輸出額外的自旋次數和耗時信息
		if (index == count) {
			System.out.println("額外的自旋次數:"+casCount.get());
			System.out.println("耗時:" + (System.currentTimeMillis() - startTime));
			System.exit(1);
		}
	}

	public static void main(String[] args) {
		CAS cas = new CAS();
		for (int i = 0; i < 5; i++) {
			new Thread(()->{
				while (true) {
					cas.incr();
				}
			}).start();
		}
	}
}
線程數 額外自旋次數 耗時(ms)
1 0 1329
2 51905576 6585
5 147004339 11325
10 218429562 11438

單線程下,由於不存在競爭關係,每次寫入都會成功,完全不需要自旋重試。
但是隨着多線程競爭的激烈程度的上升,需要自旋重試的次數不斷變多,性能也隨之下降。

只做自增的話直接調用unsafe.getAndAddInt()可以獲得更好的性能,本博客旨在幫助大家更好的理解CAS操作,遂取舊值再調用compareAndSwapInt()。

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