您好,登錄后才能下訂單哦!
這篇文章主要講解了“CAS與java樂觀鎖怎么用”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“CAS與java樂觀鎖怎么用”吧!
CAS是CompareAndSwap,即比較和交換。為什么CAS沒有用到鎖還能保證并發情況下安全的操作數據呢,名字其實非常直觀的表明了CAS的原理,具體修改數據過程如下:
從上述過程可以看到CAS其實保證的是安全的修改數據,但是修改存在失敗的可能性,即目標變量數據修改不成功,這個時候我們要循環判斷CAS修改數據結果,如果失敗進行重試。
思維比較縝密的同學可能擔心CAS本身這個比較與替換的操作產生并發安全問題,實際應用中這種情況不會發生,比較與替換由JDK借助硬件級別的CAS原語來保證比較替換是一個原子性動作。
無鎖編程指的是在不使用鎖的情況下保證安全的操作共享變量在并發編程中,我們用各種鎖來保證共享變量的安全性。即在保證一個線程未操作完共享變量的時候其他線程不能操作同一共享變量。
正確的使用鎖可以保證并發情況下數據安全,但是在并發程度不高,競爭不激烈的時候,獲取鎖和釋放鎖就成了沒必要的性能浪費。這種情況下可以可考慮利用CAS保證數據安全,實現無鎖編程
上面我們已經了解了CAS保證安全操作共享變量的原理,但是上述CAS操作還存在缺陷。假設當前線程訪問的共享變量值為A,在線程1訪問共享變量過程中,線程2操作共享變量將其賦值為B,線程2處理完自己的邏輯后又將共享變量賦值為A。這時線程1比較共享變量值A與原始值A相同,誤以為沒有其他線程操作共享變量,直接返回操作成功。這就是ABA問題。雖然大部分業務不需要關心共享變量是否有過其他更改,只要原始值與當前值一致就能得到正確的結果,但是有一些敏感場景不光要考慮共享變量結果上等同于沒有被修改過,同時也不能接受共享變量過程上被其他線程修改過。幸運的是ABA問題也有成熟的解決方案,我們為共享變量添加一個版本號,每當共享變量被修改這個版本號值就會自增。在CAS操作中我們比較的不是原始變量值,而是共享變量的版本號。每次操作共享變量更新的版本號都是唯一的,所以能夠避免ABA問題。
首先多個線程對普通變量進行并發操作是不安全的,一個線程的操作結果可能被其他線程覆蓋掉,比如現在我們用兩個線程,每個線程將初始值為1的共享變量增加一,如果沒有同步機制的話共享變量結果很可能小于3。即可能線程1和線程2都讀到了初始值1,線程1將其賦值為2,線程2所在內存讀取到的值還是1不會變,線程2也將變量增加1然后賦值成2,這樣最終結果是2小于預期結果3。自增操作不是原子性操作導致了這個共享變量操作不安全問題。為了解決這個問題,JDK提供了一系列原子類提供相應的原子操作。下面是AtomicInteger中的getAndIncrement方法源碼,讓我們從源碼來看是怎么利用CAS實現線程安全的原子性的整形變量相加操作。
/**
* 原子性的將當前值增加1
*
* @return 返回自增前的值
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
可以看到getAndIncrement實際調用了UnSafe類的getAndAddInt方法實現原子操作,下面是getAndAddInt源代碼
/**
* 原子的將給定值與目標字變量相加并重新賦值給目標變量
*
* @param o 要更新的變量所在的對象
* @param offset 變量字段的內存偏移值
* @param delta 要增加的數字值
* @return 更改前的原始值
* @since 1.8
*/
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
// 獲取當前目標目標變量值
v = getIntVolatile(o, offset);
// 這句代碼是關鍵, 自旋保證相加操作一定成功
// 如果不成功繼續運行上一句代碼, 獲取被其他
// 線程搶先修改的變量值, 在新值基礎上嘗試相加
// 操作, 保證了相加操作的原子性
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
我們都對鎖很熟悉, 比如可重入鎖ReentrantLock, JDK提供的各種鎖基本都依賴AbstractQueuedSynchronizer這個類, 當多個線程嘗試獲取鎖時會進入一個隊列等待, 其中多線程入隊操作的原子性就是用CAS來保證的. 源代碼如下:
/**
* 鎖底層等待獲取鎖的線程入隊操作
* @param node 要入隊的線程節點
* @return 入隊節點的前驅節點
*/
private Node enq(final Node node) {
// 自旋等待節點入隊, 通過cas保證并發情況下node安全正確入隊
for (;;) {
Node t = tail;
// head為空時構造dummy node初始化head和tail
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
// 如果cas設置tail失敗了
// 下個循環取到了最新的其他線程搶先設置的tail
// 繼續嘗試設置.
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
/**
* 原子性的設置tail尾節點為新入隊的節點
*/
private final boolean compareAndSetTail(Node expect, Node update) {
// 可以看到此處又是調用了Unsafe類下的原子操作方法
// 如果目標字段(tail尾節點字段)當前值是預期值
// 即沒有被其他線程搶先修改成功, 那么就設置成功
// 返回true
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
除了JDK中Uusafe類提供的各種原子性操作外,我們實際開發中可以用CAS思想保證并發情況下安全的操作數據庫。假設有user表結構以及數據如下, version字段是實現樂觀鎖的關鍵
id | user | coupon_num | version |
---|---|---|---|
1 | 朱小明 | 0 | 0 |
假設我們有一個用戶領取優惠券的按鈕,怎么防止用戶快速點擊按鈕造成重復領取優惠券的情況呢。我們要安全的更改id為1的用戶的coupon_num優惠券數量,將version字段作為CAS比較的版本號,即可避免重復增加優惠券數量,比較和替換這個邏輯通過WHERE條件來實現. 涉及sql如下:
UPDATE user
SET coupon_num = coupon_num + 1, version = version + 1
WHERE version = 0
可以看到,我們查詢出id為1的數據, 版本號為0,修改數據的同時把當前版本號當做條件即可實現安全修改,如果修改失敗,證明已經被其他線程修改過,然后看具體業務決定是否需要自旋嘗試再次修改。這里要注意考慮競爭激烈的情況下多個線程自旋導致過度的性能消耗,根據并發量選擇適合自己業務的方式
感謝各位的閱讀,以上就是“CAS與java樂觀鎖怎么用”的內容了,經過本文的學習后,相信大家對CAS與java樂觀鎖怎么用這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。