◆
CAS的概念
◆算法
對於併發控制來講,使用鎖是一種悲觀的策略。它老是假設每次請求都會產生衝突,若是多個線程請求同一個資源,則使用鎖寧肯犧牲性能也要保證線程安全。而無鎖則是比較樂觀的看待這個問題,它會假設每次訪問都沒有衝突,這樣就提升了效率。可是事實難料、這個衝突是避免不了的,無鎖也考慮到了確定會遇到衝突,對於衝突的解決無鎖就使用一種比較交換(CAS)的技術來檢測衝突。一旦檢測到衝突就重試當前操做直到成功爲止。數組
◆
CAS算法
◆安全
CAS機制中使用了3個基本操做數CAS(V,E,N):V表示要更新的變量,E表示預期值,N表示新值。bash
CAS更新一個變量的時候,只有當變量的預期值E和要更新的變量V的實際值相同時,纔會將V的值修改成N。多線程
一個簡單的例子:
在內存地址V當中,存儲一個值爲1的變量。併發
此時線程1想把變量的值增長1.對線程1來講,預期值E=1,要修改的新值N=2.性能
在線程1要提交更新以前,另外一個線程2搶先一步,把V的值率先更新成了2。ui
此時線程1開始提交更新,首先進行預期值E和變量V的實際值比較,發現E不等於V的實際值,提交失敗。this
失敗後線程1 從新獲取內存地址V的當前值,並從新計算想要修改的值。此時對線程1來講,E=2,V=2。這個從新嘗試的過程被稱爲自旋。spa
若是這一次依然在提交時發現被線程2把V值更新到了3則再次重複步驟5。此時E=3,V=3
步驟5執行執行完畢後再次更新發現沒有其餘線程改變V的值。線程1進行比較,發現A和V的值是相等的。則線程1進行交換,把V的值替換爲N,也就是2.
◆
Java中CAS的底層實現
◆
咱們看一下AtomicInteger當中經常使用的自增方法incrementAndGet:
123複製代碼 |
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}複製代碼 |
這裏涉及到兩個重要的對象,一個是unsafe,一個是valueOffset。
unsafe是什麼東西呢?它JVM爲咱們提供了一個訪問操做系統的後門,unsafe爲咱們提供了硬件級別的原子操做。而valueOffset對象,是經過unsafe.objectFiledOffset方法獲得,所表明的是AtomicInteger對象value成員變量在內存中的偏移量。咱們能夠簡單的把valueOffset理解爲value變量的內存地址。
而unsafe的getAndAddInt方法顧名思義就是使用操做系統的原子操做來爲咱們實現當前的的++操做並把舊值返回回來。由於是返回的舊值因此
incrementAndGet方法返回的數據應該是這個舊值加上1
◆
CAS的缺點
◆
CPU開銷過大
在併發量比較高的狀況下,若是許多線程反覆嘗試更新某一個變量,卻又一直更新不成功,循環往復,會給CPU帶來很到的壓力。
不能保證代碼塊的原子性
CAS機制所保證的知識一個變量的原子性操做,而不能保證整個代碼塊的原子性。好比須要保證3個變量共同進行原子性的更新,就不得不使用synchronized了。
ABA問題
這是CAS機制最大的問題所在。
複製代碼
咱們如今來講什麼是ABA問題。
假設小王帳戶有1000塊錢,即v=1000。
這時有三個線程想使用CAS的方式更新這個小王的帳戶。線程1和線程2已經獲取當前帳戶餘額爲1000,線程3還未獲取當前值。
線程1爲花唄扣款、線程2爲花唄扣款的備用操做(避免第一次扣款失敗),線程3爲工資入帳
接下來,線程1先一步執行成功,把當前帳戶成功從1000減小到500;同時線程2由於某種緣由被阻塞住,沒有及時扣款;線程3在線程1扣款以後,獲取了當前值500。
在以後,線程2仍然處於阻塞狀態,線程3繼續執行,成功入帳工資500,把當前值又變回了1000。
此時,線程2恢復運行狀態,進行更新以前查詢E和V相同,因此堅決果斷的進行又一次帳戶扣款。
這種扣款的方式對於小王來講確定是不可接受的(估計都要瘋了),解決方案就是在操做的時候加個版本號或者是時間戳來標示狀態信息。
一樣以剛纔的例子來講:
假設小王帳戶有1000塊錢,即v=1000。
這時有三個線程想使用CAS的方式更新這個小王的帳戶。線程1和線程2已經獲取當前帳戶餘額爲1000,線程3還未獲取當前值。可是呢,這裏線程1和2還須要記錄一個獲取當前帳戶餘額的最後更新時間,好比9.30.
一樣的線程1爲花唄扣款、線程2爲花唄扣款的備用操做(避免第一次扣款失敗),線程3爲工資入帳。
接下來,線程1先一步執行成功,把當前帳戶成功從1000減小到500;此時帳戶餘額的時間戳就已經變了,好比9.31。同時線程2由於某種緣由被阻塞住,沒有及時扣款;線程3在線程1扣款以後,獲取了當前值500和時間戳9.31。
在以後,線程2仍然處於阻塞狀態,線程3繼續執行,成功入帳工資500,把帳戶又變回了1000,同時時間戳更新爲9.32。
此時,線程2恢復運行狀態,進行更新以前查詢E和V雖然相同,可是時間戳確是不同的。
◆
Java提供的12種原子操做類
◆
原子更新基本類型
AtomicBoolean:原子更新布爾類型
AtomicInteger:原子更新整型
AtomicLong:原子更新長整型。複製代碼
原子更新數組
複製代碼 |
AtomicIntegerArray:原子更新整型數組裏的元素。
AtomicLongArray:原子更新長整型數組裏面的元素。
AtomicReferenceArray:原子更新引用類型數組裏的元素。複製代碼 |
原子更新引用類型
AtomicReference:原子更新引用類型。
AtomicReferenceFieldUpdater:原子更新引用類型裏的字段。
AtomicMarkableReference:原子更新帶有標記位的引用類型。複製代碼 |
原子更新字段
AtomicIntegerFieldUpdater:原子更新整型字段的更新器。
AtomicLongFieldUpdater:原子更新長整型字段的更新器。
AtomicStampedReference:原子更新帶有版本號的引用類型。複製代碼 |