並發之CAS無鎖技術
阿新 • • 發佈:2018-05-16
兩個 javap 執行 update ID get 變量 dset 變化 CAS算法即是:Compare And Swap,比較並且替換;
CAS算法存在著三個參數,內存值V,舊的預期值A,以及要更新的值B。當且僅當內存值V和預期值B相等的時候,才會將內存值修改為B,否則什麽也不做,直接返回false; 比如說某一個線程要修改某個字段的值,當這個值初始化的時候會在內存中完成,根據Java內存模型,該線程保存著這個變量的一個副本;當且僅當這個變量的副本和內存的值如果相同,那麽就可以完成對值得修改,並且這個CAS操作完全是原子性的操作,也就是說此時這個操作不可能被中斷。 先來看一個n++的問題:
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(intb) { 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來解決
並發之CAS無鎖技術