在之前文章中驗證了在多線程場景下,CAS可以保證共享變量的原子性。
此篇文章主要記錄一下CAS原理的學習感悟。
在一般情況下,為保證資料安全性,我們可以采用synchronized修飾變量或者修飾方法。也就是說在同一時間隻有一個線程能修改共享變量或者通路這個方法,其它線程都要等待。但是這樣的話,也就相當于了單線程,失去了多線程的優勢。
CAS的原理有些類似于悲觀鎖;
了解CAS之前,先了解一下悲觀鎖,樂觀鎖;
悲觀鎖&樂觀鎖
悲觀鎖:也就是往最壞的情況下考慮,假設會發送并發沖突的問題,是以當某個線程擷取到共享資源時,會阻止别的線程擷取共享資源。是以也可以稱為 獨占鎖或者互斥鎖。比如synchronized同步鎖。
樂觀鎖:故名思意,假設不會發生并發沖突的情況。隻有在最後更新修改共享資源時,才會去判斷一下在從主存中拿到資料到修改資料這段時間内有沒有别的線程修改了這個共享資源。如果發生了沖突就重試,如果沒有沖突,就更新成功。CAS是樂觀鎖的一種實作方式。
【悲觀鎖會阻塞其它線程,樂觀鎖不會阻塞其它線程,如果發生沖突,會采用循環的方式一直重試,直至成功】
CAS實作原理
CAS主要通過三個值實作
V:目前記憶體值
A:預期值
B:期待更新的值
舉個栗子解釋一下上面三個參數:
場景:現在有兩個線程同時更改共享變量56【此時目前記憶體值(V)】,兩個線程各自對共享變量進行+1的操作【期待更新的值(B)就為1】
因為線程1,線程2 同時通路同一個共享變量56,都會将主存中的值拷貝到自己的工作記憶體中【線程1,線程2的預期值(A)都是56】
假設線程1,線程2線上程競争中,線程1能去先修改變量的值。線程1對變量修改後,并更新到主存中,這時候主存中資料變成了57。
對于線程2來說,此時記憶體值變成了57,和預期值56不一緻。是以就會操作失敗?操作失敗怎麼處理呢?重新擷取記憶體中的最新值,這時候線程2的預期值就變成了57,和記憶體值相等。再進行更新操作。
此操作會采用循環的方式直至更新成功;
public final int getAndAddInt(Object obj, long valueOffset, int var) {
int expect;
// 利用循環,直到更新成功才跳出循環。
do {
// 從記憶體中擷取共享變量的最新值 valueOffset 指此共享變量在記憶體中的位置(偏移量)
expect = this.getIntVolatile(obj, valueOffset);
// expect + var表示需要更新的值,如果compareAndSwapInt傳回false,說明value值被其他線程更改了。
// 那麼就循環重試,再次擷取value最新值expect,然後再計算需要更新的值expect + var。直到更新成功
} while(!this.compareAndSwapInt(obj, valueOffset, expect, expect + var));
// 傳回目前線程在更改value成功後的,value變量原先值。并不是更改後的值
return expect;
}
就是指當兩者進行比較時,如果相等,則證明共享資料沒有被修改,替換成新值,然後繼續往下運作;如果不相等,說明共享資料已經被修改,放棄已經所做的操作,然後重新執行剛才的操作。容易看出 CAS 操作是基于共享資料不會被修改的假設,采用了類似于資料庫的commit-retry 的模式。當同步沖突出現的機會很少時,這種假設能帶來較大的性能提升。
由此可見,AtomicInteger.incrementAndGet的實作用了樂觀鎖技術,調用了類sun.misc.Unsafe庫裡面的 CAS算法,用CPU指令來實作無鎖自增。是以,AtomicInteger.incrementAndGet的自增比用synchronized的鎖效率倍增。
參考:
https://blog.csdn.net/moakun/article/details/80144900
https://www.jianshu.com/p/a142350e9b7a