CAS算法即是:Compare And Swap,比較並且替換;
CAS算法存在着三個參數,內存值V,舊的預期值A,以及要更新的值B。當且僅當內存值V和預期值B相等的時候,才會將內存值修改為B,否則什么也不做,直接返回false;
比如說某一個線程要修改某個字段的值,當這個值初始化的時候會在內存中完成,根據Java內存模型,該線程保存着這個變量的一個副本;當且僅當這個變量的副本和內存的值如果相同,那么就可以完成對值得修改,並且這個CAS操作完全是原子性的操作,也就是說此時這個操作不可能被中斷。
先來看一個n++的問題:
public class Case { public volatile int n; public void add() { n++; } }
上述代碼中什么變量被volatile修飾,此時說明該變量在多線程操作的情況下可以保證內存的可見性,但是不可以保證原子性操作,因此在多線程並發的時候還是會出現問題的;利用Javap命令來看看匯編指令:
PS D:\ssh> javac Case.java
PS D:\ssh> javap -c Case
Compiled from "Case.java"
public class Case {
public volatile int n;
public Case();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void add();
Code:
0: aload_0
1: dup
2: getfield #2 // Field n:I
5: iconst_1
6: iadd
7: putfield #2 // Field n:I
10: return
}
PS D:\ssh>
在方法add()中,第17行表示獲取到了n的初始值;
第19行執行了iadd()操作,n加一;
第20行執行了putfield,把新累加的值賦值給n;
在上面我很清楚的說過volatile確實無法保證上述三個操作步驟的原子性;可以使用synchrnoized的方法完成原子性的操作;synchrnoized是互斥鎖,也是可重入的鎖,可以保證操作的原子性;但是加鎖之后效率降低,
好了,接下來再看一段代碼:
public int a = 1; public boolean compareAndSwapInt(int b) { if (a == 1) { a = b; return true; } return false; }
上述方法在並發的情況下也是會出現問題的;當多個線程直接進入compareAndSwapInt()之后,他們也同時符合上述的邏輯判斷,此時對a的賦值也有可能同事發生,這樣也帶來了線程安全的問題;
同樣加鎖的方式也可以解決這個問題,但是在這里我們不研究鎖的問題;下面我們來看看一段代碼,這是AtomicInteger類中的一部分源碼:
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; /** * Gets the current value. * * @return the current value */
public final int get() { return value; } }
1 Unasfe是CAS的核心類,通過這個類可以獲取字段在內存中的地址偏移量;Unsafe是native的,我們一般不可能使用;這是Java對硬件操作的支持;
2 valueOffset是地址偏移量(變量在內存中的地址偏移量)
3 value是使用volatile修飾的,保證了內存的可見性;
平時做常用的方法addAndGet()方法;作用是原子性的操作給變量添加值;
int |
addAndGet(int delta) 以原子方式將給定值與當前值相加。 |
在Java8中,這個方法的實現是調用了unsafe()方法;因此我們看不到;
public final int addAndGet(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta) + delta; }
但是通過網上看到了該方法的實現方式:
public final int addAndGet(int delta) { for (;;) { int current = get(); int next = current + delta; if (compareAndSet(current, next)) return next; } }
public final int get() { return value; }
假設delta的值為1,在CAS算法下操作的話,首先進入一個for循環體;假設存在着兩個線程,並且內存中的值value=3;根據Java內存模型,每一個線程都存在這這個變量的副本;
1) 線程1進入循環體,獲取到current的值為3,然后獲取到到next的值此時為4;此時假設線程1運氣不好,被掛起;
2)線程2進入循環體,獲取到current的值為3,同時next的值也為4;線程2運氣好,此時繼續執行compareAndSet()方法;
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
線程2傳入兩個參數,一個當前值,以及一個預期值;當前值,也就是current=3.要修改成為4;此時當前值也就是預期值和內存中的value比較,此時都是3,那么修改內存中的值為4;
3)線程1此時再次執行compareAndSwapInt()方法的時候。發現內存中的值為4,預期的值是3,兩者不相等,此時就不可以再次賦值了;
CAS的缺點:
CAS存在和“ABA的漏洞”;什么是ABA呢?
假定在某個時刻某個線程從內存中取出A,然后在下個時刻准備更新這個值;在這個時間差內數據發生了改變;

假設線程1從內存中取出了A,線程2也從內存中取出了A,並且將值修改為B,最后又改為A,當線程1去更新值得時候發現內存中的數據和線程備份數據相同,可以更新;但是此時內存中的值其實發生了變化的,只不過又變回去了;在實際的開發過程中,ABA可能會帶來一些問題,但是我認為無關緊要,我們需要的只是數值的變化而已;
對於單向鏈表實現的棧而言;假設存在一個鏈表 A---->B;線程1要去將棧頂的數據修改為B,但是此時線程2進來之后,A---->B出棧,D、C、A壓棧;此時鏈表的結構發生了變化;A---->C---->D;此時線程1發現棧頂元素還是A,而元素B被出棧之后成為一個游離的對象,
解決方式:由於CAS算法沒有直接的使用鎖;而是通過樂觀鎖的方式去控制並發的;而對於樂觀鎖而言一般都是操作+時間戳來控制每一次的版本號的;在JDK類庫中,可以使用AutomicStampReference來解決