Java并發基礎 - CAS篇
CAS(Compare and Swap),中文可以理解為比較并替換,是一種實現并發算法時常用到的技術。它是一種無鎖原子算法,是一種樂觀鎖的實現方式,在操作時是抱著樂觀的態度進行的,它總是認為可以成功完成操作。
CAS思路
讓我們先直觀的理解一下CAS的大體思路:
CAS(V,E,N)
它包含 3 個參數,V表示要更新變量的值,E表示預期值,N表示新值。僅當 V值等于E值時,才會將V的值設為N,如果V值和E值不同,則說明已經有其他線程做過更新,則當前線程則什么都不做。最后,CAS 返回當前V的真實值。
先從一個簡單例子開始:
public class CasTest0 { private static volatile int m = 0; public static void increase1() { m++; } public static void main(String[] args) throws Exception { for (int i = 0; i < 1000; i++) { new Thread(() -> { CasTest0.increase1(); }).start(); } TimeUnit.SECONDS.sleep(3); System.out.println(m); } } public class CasTest0 { private static volatile int m = 0; public static void increase1() { m++; } public static void main(String[] args) throws Exception { for (int i = 0; i < 1000; i++) { new Thread(() -> { CasTest0.increase1(); }).start(); } TimeUnit.SECONDS.sleep(3); System.out.println(m); } }
運行這個例子,最后打印出的值可能是任意小于1000的正整數。在之前的文章中講過,volatile可以保證可見性和有序性,但是無法保證原子性。而m++這一操作又不是原子操作,可以分為三個步驟:
獲取靜態變量m的值并入棧,int型常量值1入棧
棧頂int型數值相加,并將結果壓入棧頂
為靜態變量m賦值
從上述分析可得,自增操作并不具有原子性,所以在多線程環境下,運行得到的結果必定小于等于1000。
JUC中的CAS實現
接下來,換成JUC包下的原子類操作試一下:
public class CasTest1 { private static AtomicInteger atomicI = new AtomicInteger(0); public static void increase2() { atomicI.incrementAndGet(); } public static void main(String[] args) throws Exception { for (int i = 0; i < 1000; i++) { new Thread(() -> { CasTest1.increase2(); }).start(); } TimeUnit.SECONDS.sleep(3); System.out.println(atomicI.get()); } } public class CasTest1 { private static AtomicInteger atomicI = new AtomicInteger(0); public static void increase2() { atomicI.incrementAndGet(); } public static void main(String[] args) throws Exception { for (int i = 0; i < 1000; i++) { new Thread(() -> { CasTest1.increase2(); }).start(); } TimeUnit.SECONDS.sleep(3); System.out.println(atomicI.get()); } }
這樣運行結果會始終返回1000,這就取決于原子Integer類AtomicInteger發揮了作用。反編譯看一下實際執行的指定,這里是僅以一條指令完成了自增的操作
看一下AtomicInteger類的實現:
在AtomicInteger中使用了Unsafe類,這個類可以說是java提供的一個后門類,可以用來直接操作內存地址。(針對Unsafe,以前寫過一篇專門的文章,大家如果有需求可以看一下Java雙刃劍之Unsafe類詳解)
代碼中的變量表示的意義如下:
valueOffset:變量value的地址偏移量,具體賦值是在下面的靜態代碼塊中進行的
value:需要修改的值,相當于i++操作中的i
incrementAndGet最終調用Unsafe類中的方法:
//獲取內存地址為obj+offset的變量值, 并將該變量值加上delta public final int getAndAddInt(Object obj, long offset, int delta) { int v; do { //通過對象和偏移量獲取變量的值 //由于volatile的修飾, 所有線程看到的v都是一樣的 v= this.getIntVolatile(obj, offset); } while(!this.compareAndSwapInt(obj, offset, v, v + delta)); return v; }
具體流程:
while循環中的compareAndSwapInt()方法嘗試修改v的值, 該方法會通過obj和offset獲取變量的值
如果這個值和v不一樣,說明其他線程修改了obj+offset地址處的值,此時compareAndSwapInt()返回false,繼續循環
如果這個值和v一樣,說明沒有其他線程修改obj+offset地址處的值,此時可以將obj+offset地址處的值改為v+delta, compareAndSwapInt()返回true,退出循環
compareAndSwapInt是一個native方法,調用了c++中的方法,后續調用鏈為調用匯編中的cmpxchg指令,最終通過二進制硬件支持實現了這一原子操作。
那么,CAS都有什么應用場景呢?典型場景就是電商中對于貨物的庫存的管理。首先從數據庫中讀取庫存,在賣出貨物后更新庫存時,判斷庫存數量是否還和自己取出時相同,如果相同則更新,不同則進行自旋直到執行成功。
ABA問題
說了這么多,那么CAS就是完美的嗎,很遺憾并不是,CAS仍然存在經典的ABA問題:
按照我們之前的理解,CAS需要檢查操作值有沒有發生改變,如果沒有發生改變則更新。但是存在這樣一種情況:如果一個值原來是A,變成了B,然后又變成了A,那么在CAS檢查的時候會發現沒有改變,但是實質上它已經發生了改變,這就是所謂的ABA問題。
public class CasTest2 { private static AtomicInteger atomicI = new AtomicInteger(100); public static void main(String[] args) throws Exception { Thread t1 = new Thread(() -> { System.out.println(Thread.currentThread().getName()+":"+atomicI.compareAndSet(100, 110)); },"thread1"); t1.start(); Thread t2 = new Thread(new Runnable() { @Override public void run() { try { TimeUnit.SECONDS.sleep(1); System.out.println(Thread.currentThread().getName()+":"+atomicI.compareAndSet(110, 100)); } catch (InterruptedException e) { e.printStackTrace(); } } },"thread2"); t2.start(); Thread t3 = new Thread(() -> { try { TimeUnit.SECONDS.sleep(3); System.out.println(Thread.currentThread().getName()+":"+atomicI.compareAndSet(100, 90)); } catch (InterruptedException e) { e.printStackTrace(); } },"thread3"); t3.start(); } }
運行結果:
三個線程的運行結果都為true,但是thread3執行時候檢查的值是已經被中途修改過的,而不是初始值了。
應對ABA問題,其解決方案是加上版本號,即在每個變量都加上一個版本號,每次改變時加1。
即將原來的:A —> B —> A 變成:1A —> 2B —> 3A
這里引入AtomicStampedReference這個類,它內部不僅維護了對象值,還維護了一個int類型的stamp值,可以將其理解為時間戳或版本號。當AtomicStampedReference對應的數值被修改時,除了更新數據本身外,還必須要更新這個stamp的值。并且當AtomicStampedReference設置對象值時,對象值以及stamp值都必須滿足期望,寫入才會成功。因此,即使對象值被反復讀寫,寫回原值,只要stamp的值發生變化,就能防止不恰當的寫入。
public class CasTest3 { private static AtomicStampedReference asr = new AtomicStampedReference(100, 1); public static void main(String[] args) throws Exception { Thread t1 = new Thread(() -> { try { TimeUnit.SECONDS.sleep(2); } catch (Exception e) { e.printStackTrace(); } System.out.println("1:" + asr.compareAndSet(100, 110, asr.getStamp(), asr.getStamp() + 1) ); System.out.println("stamp:"+asr.getStamp()+" value:"+asr.getReference()); System.out.println("2:" + asr.compareAndSet(110, 100, asr.getStamp(), asr.getStamp() + 1) ); System.out.println("stamp:"+asr.getStamp()+" value:"+asr.getReference()); }); Thread t2 = new Thread(() -> { int stamp = asr.getStamp(); try { TimeUnit.SECONDS.sleep(4); } catch (Exception e) { e.printStackTrace(); } System.out.println("3:" + asr.compareAndSet(100, 110, stamp, stamp + 1) ); System.out.println("stamp:"+asr.getStamp()+" value:"+asr.getReference()); }); t1.start(); t2.start(); } }
運行結果:
Thread2期待的stamp值為1,Reference的值為100。Thread1在每次自增的同時,stamp值加1,所以在經過Thread1兩次修改Reference值后,即使與期望的Reference值相同,但stamp值不同,仍然不做任何修改。
CAS缺點
最后對CAS的缺點進行一下總結,CAS雖然高效地解決了原子操作問題,但是還是存在一些缺陷的,主要表現在三個方面:
循環時間太長:如果CAS一直不成功的情況發生,會一直進行自旋操作,會造成大量CPU執行開銷。在JUC中有些地方就限制了CAS自旋的次數,例如BlockingQueue的SynchronousQueue
只能保證一個共享變量原子操作:看了CAS的實現過程,可以得出CAS只能針對一個共享變量,如果是多個共享變量情況,只能使用鎖來保證原子性了
ABA問題:CAS需要檢查操作值有沒有發生改變,如果沒有發生改變則更新,但是之前提到的ABA問題會造成一定影響,這時只要加上版本號對其進行限定就可以了
Java 多線程
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。