併發程式設計-硬體加持的CAS操作夠快麼?
Talk is cheap
CAS(Compare And Swap),即比較並交換。是解決多執行緒並行情況下使用鎖造成效能損耗的一種機制,CAS操作包含三個運算元——記憶體位置(V)、預期原值(A)和新值(B)。如果記憶體位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論位置V的值是否等於A, 都將返回V原有的值。
CAS的含義是”我認為V的值應該是A,如果是,那我將V的值更新為B,否則不修改並告訴V的值實際是多少“
Show you my code
在單執行緒環境中分別使用無鎖,加鎖以及cas進行十組5億次累加運算,然後打印出平均耗時。
/** * cas對比加鎖測試 * * @author Jann Lee * @date 2019-11-21 0:12 **/ public class CasTest { @Test public void test() { long times = 500_000_000; // 記錄耗時 List<Long> elapsedTime4NoLock = new ArrayList<>(10); List<Long> elapsedTime4Synchronized = new ArrayList<>(10); List<Long> elapsedTime4ReentrantLock = new ArrayList<>(10); List<Long> elapsedTime4Cas = new ArrayList<>(10); // 進行10組試驗 for (int j = 0; j < 10; j++) { // 無鎖 long startTime = System.currentTimeMillis(); for (long i = 0; i < times; i++) { } long endTime = System.currentTimeMillis(); elapsedTime4NoLock.add(endTime - startTime); // synchronized 關鍵字(隱式鎖) startTime = endTime; for (long i = 0; i < times; ) { i = addWithSynchronized(i); } endTime = System.currentTimeMillis(); elapsedTime4Synchronized.add(endTime - startTime); // ReentrantLock 顯式鎖 startTime = endTime; ReentrantLock lock = new ReentrantLock(); for (long i = 0; i < times; ) { i = addWithReentrantLock(i, lock); } endTime = System.currentTimeMillis(); elapsedTime4ReentrantLock.add(endTime - startTime); // cas(AtomicLong底層是用cas實現) startTime = endTime; AtomicLong atomicLong = new AtomicLong(); while (atomicLong.getAndIncrement() < times) { } endTime = System.currentTimeMillis(); elapsedTime4Cas.add(endTime - startTime); } System.out.println("無鎖計算耗時: " + average(elapsedTime4NoLock) + "ms"); System.out.println("synchronized計算耗時: " + average(elapsedTime4Synchronized) + "ms"); System.out.println("ReentrantLock計算耗時: " + average(elapsedTime4ReentrantLock) + "ms"); System.out.println("cas計算耗時: " + average(elapsedTime4Cas) + "ms"); } /** * synchronized加鎖 */ private synchronized long addWithSynchronized(long i) { i = i + 1; return i; } /** * ReentrantLock加鎖 */ private long addWithReentrantLock(long i, Lock lock) { lock.lock(); i = i + 1; lock.unlock(); return i; } /** * 計算平均耗時 */ private double average(Collection<Long> collection) { return collection.stream().mapToLong(i -> i).average().orElse(0); } }
從案例中我們可能看出在單執行緒環境場景下cas的效能要高於鎖相關的操作。當然,在競爭比較激烈的情況下效能可能會有所下降,因為要不斷的重試和回退或者放棄操作,這也是CAS的一個缺點所在,因為這些重試,回退等操作通常用開發者來實現。
CAS的實現並非是簡單的程式碼層面控制的,而是需要硬體的支援,因此在不同的體系架構之間執行的效能差異很大。但是一個很管用的經驗法則是:在大多數處理器上,在無競爭的鎖獲取和釋放的”快速程式碼路徑“上的開銷,大約是CAS開銷的兩倍。
為何CAS如此優秀
硬體加持,現代大多數處理器都從硬體層面通過一些列指令實現CompareAndSwap(比較並交換)同步原語,進而使作業系統和JVM可以直接使用這些指令實現鎖和併發的資料結構。我們可以簡單認為,CAS是將比較和交換合成是一個原子操作。
JVM對CAS的支援, 由於Java程式執行在JVM上,所以應對不同的硬體體系架構的處理則需要JVM來實現。在不支援CAS操作的硬體上,jvm將使用自旋鎖來實現。
CAS的ABA問題
cas操作讓我們減少了鎖帶來的效能損耗,同時也給我們帶來了新的麻煩-ABA問題。
線上程A讀取到x的值與執行CAS操作期間,執行緒B對x執行了兩次修改,x的值從100變成200,然後再從200變回100;而後線上程A執行CAS操作過程中並未發現x發生過變化,成功修改了x的值。由於x的值100 ->200->100,所以稱之為ABA的原因。
魔高一尺道高一丈,解決ABA的問題目前最常用的辦法就是給資料加上“版本號”,每次修改資料時同時改變版本號即可。
Q&A
在競爭比較激烈的情況下,CAS要進行回退,重試等操作才能得到正確的結果,那麼CAS一定比加鎖效能要高嗎?