CAS
CAS原理
CAS:Compare and Swap(Compare and Swap 比較並交換)是樂觀鎖技術。CAS有3個操作數:內存值V、預期值A、要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改爲B,否則什麼都不做。該操作是一個原子操作,被廣泛的應用在Java的底層實現中。在Java中,CAS主要是由sun.misc.Unsafe這個類通過JNI調用CPU底層指令實現
當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其他線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。
CAS操作中包含三個操作數——需要讀寫的內存位置(V)、進行比較的預期原值(A)和擬寫入的新值(B)。如果內存位置V的值與預期原值A相匹配,那麼處理器會自動將該位置值更新爲新值B,否則處理器不做任何操作。無論哪種情況,它都會在CAS指令之前返回該位置的值(在CAS的一些特殊情況下將僅返回CAS是否成功,而不提取當前值)。CAS有效地說明了“我認爲位置V應該包含值A;如果包含該值,則將B放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可”。這其實和樂觀鎖的衝突檢查+數據更新的原理是一樣的。
根據從上面的概念描述我們可以發現:
- 悲觀鎖適合寫操作多的場景,先加鎖可以保證寫操作時數據正確。
- 樂觀鎖適合讀操作多的場景,不加鎖的特點能夠使其讀操作的性能大幅提升。
光說概念有些抽象,我們來看下樂觀鎖和悲觀鎖的調用方式示例:
JAVA對CAS的支持
Java原子類中的遞增操作就通過CAS自旋實現的。
在JDK1.5中新增java.util.concurrent包就是建立在CAS之上的。相對於synchronized這種阻塞算法,CAS是非阻塞算法的一種常見實現。所以java.util.concurrent包中的AtomicInteger爲例,看一下在不使用鎖的情況下是如何保證線程安全的。主要理解getAndIncrement方法,該方法的作用相當於++i操作。
public class AtomicInteger extends Number implements java.io.Serializable{
private volatile int value;
public final int get(){
return value;
}
/*
*循環的內容是
* 1.取得當前值
* 2.計算+1後的值
* 3.如果預期值等於 current 等於 內存值 this,值更新爲 要修改的值 next
* 4.如果設置沒成功(當前值已經無效了即被別的線程改過了), 一直自旋
*/
public final int getAndIncrement(){
for (;;){
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
public final boolean compareAndSet(int expect, int update){
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}
CAS三大問題
CAS雖然很高效,但是它也存在三大問題,這裏也簡單說一下:
1. ABA問題。CAS需要在操作值的時候檢查內存值是否發生變化,沒有發生變化纔會更新內存值。但是如果內存值原來是A,後來變成了B,然後又變成了A,那麼CAS進行檢查時會發現值沒有發生變化,但是實際上是有變化的。ABA問題的解決思路就是在變量前面添加版本號,每次變量更新的時候都把版本號加一,這樣變化過程就從“A-B-A”變成了“1A-2B-3A”。
JDK從1.5開始提供了AtomicStampedReference類來解決ABA問題,具體操作封裝在compareAndSet()中。compareAndSet()首先檢查當前引用和當前標誌與預期引用和預期標誌是否相等,如果都相等,則以原子方式將引用值和標誌的值設置爲給定的更新值。
2. 循環時間長開銷大。CAS操作如果長時間不成功,會導致其一直自旋,給CPU帶來非常大的開銷。
3. 只能保證一個共享變量的原子操作。對一個共享變量執行操作時,CAS能夠保證原子操作,但是對多個共享變量操作時,CAS是無法保證操作的原子性的。
Java從1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,可以把多個變量放在一個對象裏來進行CAS操作。
樂觀鎖、悲觀鎖
概念
鎖分類 |
概述 |
使用場景 |
樣例 |
悲觀鎖 |
悲觀鎖:對數據被外界修改持保守態度(悲觀),因此在整個數據處理過程中,將數據處於鎖定狀態,往往依靠數據庫提供的鎖機制實現 |
寫多讀少,保證數據安全 |
行鎖、頁鎖、表鎖、共享鎖、排他鎖(寫鎖) |
樂觀鎖 |
樂觀鎖假設認爲數據一般情況下不會造成衝突,所以在數據進行提交更新的時候,纔會正式對數據的衝突與否進行檢測。如果發現衝突了,則讓返回用戶錯誤的信息,讓用戶覺得如何去做或者程序自動取重試 |
讀多寫少,提高系統吞吐量 |
數據庫樂觀鎖、緩存樂觀鎖 |
樂觀鎖、悲觀鎖到底誰的吞吐量大?
樂觀鎖只是在更新數據那一刻鎖表,其他時間不鎖表,所以相對於悲觀鎖,吞吐量更高
樂觀鎖、悲觀鎖 高併發 選擇?
在深入理解java虛擬機中提到,輕量級鎖一般情況下是優於重量級鎖(互斥鎖)的;
如果在高併發鎖競爭比較激烈的情況下輕量級鎖會由於長時間自旋消耗cpu 從而使得輕量級鎖的性能比傳統的重量級鎖更慢。
那麼樂觀鎖中也有自旋和cas,所以高併發下樂觀鎖好像不是一種好的解決方案。
樂觀鎖怎麼實現?
1.基於mysql通過版本號實現
2.基於mysql通過狀態實現
(數據庫樂觀鎖實現的有點在於簡單高效、穩定可靠;缺點在於併發能力低)
update goods set amount = amount - #{buys},version = version + 1 where code = #{code} and version = #{version}
update goods set amount = amount - #{buys} where code = #{code} and amount - #{buys} >= 0
3.基於緩存的cas機制實現
基於redis的實現:利用watch指令在redis事務中提供CAS的能力
基於memcached的實現:1.2.4版本新增特性,通過gets和cas實現
1.1 讀取數據
|
1.2 比較版本
|
1.3 更新數據