首頁 >Java >java教程 >CAS與java樂觀鎖怎麼用

CAS與java樂觀鎖怎麼用

王林
王林轉載
2023-05-01 20:07:161128瀏覽

什麼是CAS

CAS是CompareAndSwap,即比較和交換。為什麼CAS沒有用到鎖還能保證並發情況下安全的操作數據呢,名字其實非常直觀的表明了CAS的原理,具體修改數據過程如下:

  1. 用CAS操作資料時,將資料原始值和要修改的值一併傳遞給方法

  2. 比較目前目標變數值與傳進去的原始值是否相同

  3. #如果相同,表示目標變數沒有被其他執行緒修改,直接修改目標變數值即可

  4. #如果目標變數值與原始值不同,那麼證明目標變數已經被其他線程修改過,本次CAS修改失敗

從上述過程可以看到CAS其實保證的是安全的修改數據,但是修改存在失敗的可能性,即目標變量數據修改不成功,這個時候我們要循環判斷CAS修改資料結果,如果失敗重試。

思維比較縝密的同學可能擔心CAS本身這個比較與替換的操作產生並發安全問題,實際應用中這種情況不會發生,比較與替換由JDK借助硬件級別的CAS原語來保證比較替換是一個原子性動作。

CAS實作無鎖定程式設計

無鎖定程式指的是不使用鎖定的情況下保證安全的操作共享變數在並發程式設計中,我們用各種鎖來保證共享變數的安全性。即在保證一個執行緒未操作完共享變數的時候其他執行緒不能操作同一共享變數。
正確的使用鎖可以保證並發情況下資料安全,但是在並發程度不高,競爭不激烈的時候,取得鎖和釋放鎖就成了沒必要的效能浪費。這種情況下可以考慮利用CAS確保資料安全,實現無鎖定編程 

頭痛的ABA問題

上面我們已經了解了CAS保證安全操作共享變數的原理,但是上述CAS操作還存在缺陷。假設目前執行緒存取的共享變數值為A,在執行緒1存取共享變數過程中,執行緒2操作共享變數將其賦值為B,執行緒2處理完自己的邏輯後又將共享變數賦值為A。這時線程1比較共享變量值A與原始值A相同,誤以為沒有其他線程操作共享變量,直接返回操作成功。這就是ABA問題。雖然大部分業務不需要關心共享變數是否有過其他更改,只要原始值與當前值一致就能得到正確的結果,但是有一些敏感場景不光要考慮共享變數結果上等同於沒有被修改過,同時也不能接受共享變數過程上被其他執行緒修改過。幸運的是ABA問題也有成熟的解決方案,我們為共享變數加上一個版本號,每當共享變數被修改這個版本號值就會自增。在CAS運算中我們比較的不是原始變數值,而是共享變數的版本號碼。每次運算共享變數更新的版本號碼都是唯一的,所以能夠避免ABA問題。

具體應用場景 

JDK中的CAS應用

首先多個執行緒對普通變數進行並發操作是不安全的,一個執行緒的操作結果可能被其他執行緒覆蓋掉,例如現在我們用兩個線程,每個線程將初始值為1的共享變數增加一,如果沒有同步機制的話共享變數結果很可能小於3。即可能線程1和線程2都讀到了初始值1,線程1將其賦值為2,線程2所在內存讀取到的值還是1不會變,線程2也將變量增加1然後賦值成2,這樣最終結果是2小於預期結果3。自增操作不是原子性操作導致了這個共享變數操作不安全問題。為了解決這個問題,JDK提供了一系列原子類提供相應的原子操作。下面是AtomicInteger中的getAndIncrement方法原始碼,讓我們從原始碼來看是怎麼利用CAS實作執行緒安全的原子性的整形變數相加操作。

<code>/**<br> * 原子性的将当前值增加1<br> *<br> * @return 返回自增前的值<br> */<br>public final int getAndIncrement() {<br>    return unsafe.getAndAddInt(this, valueOffset, 1);<br>}<br></code>
 

可以看到getAndIncrement實際呼叫了UnSafe類別的getAndAddInt方法實作原子操作,以下是getAndAddInt原始碼

<code>/**<br> * 原子的将给定值与目标字变量相加并重新赋值给目标变量<br> *<br> * @param o 要更新的变量所在的对象<br> * @param offset 变量字段的内存偏移值<br> * @param delta 要增加的数字值<br> * @return 更改前的原始值<br> * @since 1.8<br> */<br>public final int getAndAddInt(Object o, long offset, int delta) {<br>    int v;<br>    do {<br>    	// 获取当前目标目标变量值<br>        v = getIntVolatile(o, offset);<br>    // 这句代码是关键, 自旋保证相加操作一定成功<br>    // 如果不成功继续运行上一句代码, 获取被其他<br>    // 线程抢先修改的变量值, 在新值基础上尝试相加<br>    // 操作, 保证了相加操作的原子性<br>    } while (!compareAndSwapInt(o, offset, v, v + delta));<br>    return v;<br>}<br></code>

我們都對鎖很熟悉, 例如可重入鎖ReentrantLock, JDK提供的各種鎖基本上都依賴AbstractQueuedSynchronizer這個類別, 當多個線程嘗試獲取鎖時會進入一個隊列等待, 其中多線程入隊操作的原子性就是用CAS來保證的. 原始碼如下:

<code>/**<br> * 锁底层等待获取锁的线程入队操作<br> * @param node 要入队的线程节点<br> * @return 入队节点的前驱节点<br> */<br>private Node enq(final Node node) {<br>// 自旋等待节点入队, 通过cas保证并发情况下node安全正确入队<br>    for (;;) {<br>        Node t = tail;<br>        // head为空时构造dummy node初始化head和tail<br>        if (t == null) {<br>            if (compareAndSetHead(new Node()))<br>                tail = head;<br>        } else {<br>            node.prev = t;<br>            // 如果cas设置tail失败了<br>            // 下个循环取到了最新的其他线程抢先设置的tail<br>            // 继续尝试设置.<br>            if (compareAndSetTail(t, node)) {<br>                t.next = node;<br>                return t;<br>            }<br>        }<br>    }<br>}<br>/**<br> * 原子性的设置tail尾节点为新入队的节点<br> */<br>private final boolean compareAndSetTail(Node expect, Node update) {<br>// 可以看到此处又是调用了Unsafe类下的原子操作方法<br>// 如果目标字段(tail尾节点字段)当前值是预期值<br>// 即没有被其他线程抢先修改成功, 那么就设置成功<br>// 返回true<br>    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);<br>}</code>  
  

企業開發中的樂觀鎖應用

除了JDK中Uusafe類別提供的各種原子性操作外,我們實際開發中可以用CAS思想保證並發情況下安全的操作資料庫。 假設有user表結構以及資料如下,version欄位是實作樂觀鎖的關鍵

##usercoupon_numversion#1朱小明#0 0

假设我们有一个用户领取优惠券的按钮,怎么防止用户快速点击按钮造成重复领取优惠券的情况呢。我们要安全的更改id为1的用户的coupon_num优惠券数量,将version字段作为CAS比较的版本号,即可避免重复增加优惠券数量,比较和替换这个逻辑通过WHERE条件来实现. 涉及sql如下:

<code>UPDATE user <br>SET coupon_num = coupon_num + 1, version = version + 1 <br>WHERE version = 0</code>

可以看到,我们查询出id为1的数据, 版本号为0,修改数据的同时把当前版本号当做条件即可实现安全修改,如果修改失败,证明已经被其他线程修改过,然后看具体业务决定是否需要自旋尝试再次修改。这里要注意考虑竞争激烈的情况下多个线程自旋导致过度的性能消耗,根据并发量选择适合自己业务的方式

#id

以上是CAS與java樂觀鎖怎麼用的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除