Java中有各式各樣的鎖,主流的鎖和概念以下:java
這篇文章主要是爲了讓你們經過樂觀鎖和悲觀鎖出發,理解CAS算法,由於CAS是整個Concurrent包的基礎。算法
首先,java和數據庫中都有這種概念,他只是一種廣義的概念(從線程同步的角度上看):數據庫
悲觀鎖:悲觀的認爲本身在使用數據的時候必定有別的線程來修改數據,所以在獲取數據的時候會先加鎖,確保數據不會被別的線程修改。Java中,synchronized關鍵字和Lock的實現類都是悲觀鎖。編程
樂觀鎖:樂觀的認爲本身在使用數據時不會有別的線程修改數據,因此不會添加鎖,只是在更新數據的時候去判斷以前有沒有別的線程更新了這個數據。若是這個數據沒有被更新,當前線程將本身修改的數據成功寫入。若是數據已經被其餘線程更新,則根據不一樣的實現方式執行不一樣的操做(例如報錯或者自動重試)。bash
根據從上面的概念描述咱們能夠發現:併發
從代碼層面理解:post
悲觀鎖:性能
// ------------------------- 悲觀鎖的使用方法 -------------------------
// synchronized
public synchronized void testMethod() {
// 操做同步資源
}
// ReentrantLock
private ReentrantLock lock = new ReentrantLock(); // 須要保證多個線程使用的是同一個鎖
public void modifyPublicResources() {
lock.lock();
// 操做同步資源
lock.unlock();
}複製代碼
樂觀鎖:ui
private AtomicInteger atomicInteger = new AtomicInteger(); // 須要保證多個線程使用的是同一個AtomicInteger
atomicInteger.incrementAndGet(); //執行自增1複製代碼
悲觀鎖基本上比較好理解:就是在顯示的鎖定資源後再操做同步資源。this
那麼問題來了:
樂觀鎖不鎖定資源是如何實現線程同步的呢?
答案是CAS
CAS全稱 Compare And Swap(比較與交換),本質上是一種無鎖算法:就是在沒有鎖的狀況下實現同步。
CAS相關的三個操做數:
當且僅當 V 的值等於 A 時,CAS經過原子方式用新值B來更新V的值(「比較+更新」總體是一個原子操做),不然不會執行任何操做。通常狀況下,「更新」是一個不斷重試的操做。
先看一下基本的定義:
什麼是unsafe呢?Java沒辦法直接訪問底層操做系統,可是JVM爲咱們提供了一個後門,它後門就是unsafe。unsafe爲咱們提供了硬件級別的原子操做。
對於valueOffset對象,是經過unsafe.objectFieldOffset方法獲得,所表明的是AtomicInteger對象value成員變量在內存中的偏移量。咱們能夠簡單地把valueOffset理解爲value變量的內存地址。
接下來看incrementAndGet:
// ------------------------- JDK 8 -------------------------
// AtomicInteger 自增方法
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// Unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}複製代碼
getAndAddInt方法的入參:var1是aotmicInteger對象,var2是valueOffset,var4是1;它本質上在循環獲取對象aotmicInteger中的偏移量(即valueoffset)處的值var5,而後判斷內存的值是否等於var5;若是相等則將內存的值設置爲var5+1;不然繼續循環重試,直到設置成功時再推出循環。
CAS操做封裝在compareAndSwapInt方法內部,在JNI裏是藉助於一個CPU指令完成的,屬於原子操做,能夠保證多個線程都可以看到同一個變量的修改值。後續JDK經過CPU的cmpxchg指令,去比較寄存器中的 A 和 內存中的值 V。若是相等,就把要寫入的新值 B 存入內存中。若是不相等,就將內存值 V 賦值給寄存器中的值 A。而後經過Java代碼中的while循環再次調用cmpxchg指令進行重試,直到設置成功爲止。
這裏native方法比較多,若是以爲不太好理解,咱們能夠通俗的總結一下:
循環體當中作了三件事:
1.獲取當前值。 (經過volatile關鍵字保證可見性)
2.計算出目標值:本例子中爲當前值+1
3.進行CAS操做,若是成功則跳出循環,若是失敗則重複上述步驟。
什麼是ABA呢?
由於CAS中的當前值和目標值都是隨機的,假設內存中有一個值爲A的變量,存儲在地址V當中:
內存地址V→A
此時有三個線程想使用CAS的方式更新這個變量值,每一個線程的執行時間有略微的誤差。線程1和線程2已經得到當前值,線程3還未得到當前值。
線程1:獲取到了A,計算目標值,指望更新爲B
線程2:獲取到了A,計算目標值,指望更新爲B
線程3:尚未獲取到當前值
接下來,線程1先一步執行成功,把當前值成功從A更新爲B;同時線程2由於某種緣由被阻塞住,沒有作更新操做;線程3在線程1更新以後,得到了當前值B。
內存地址V→B
線程1:獲取到了A,成功更新爲B
線程2:獲取到了A,計算目標值,指望更新爲B,Block
線程3:獲取當前值B,計算目標值,指望更新爲A
線程2仍然處於阻塞狀態,線程3繼續執行,成功把當前值從B更新成了A。
內存地址V→A
線程1:獲取到了A,成功更新爲B,已返回
線程2:獲取到了A,計算目標值,指望更新爲B,Block
線程3:獲取當前值B,成功更新爲A
最後,線程2終於恢復了運行狀態,因爲阻塞以前已經得到了「當前值」A,而且通過compare檢測,內存地址V中的實際值也是A,因此成功把變量值A更新成了B。
內存地址V→B
線程1:獲取到了A,成功更新爲B,已返回
線程2:獲取到了「當前值」A,成功更新爲B
線程3:獲取當前值B,成功更新爲A,已返回
這個過程當中,線程2獲取到的變量值A是一箇舊值,儘管和當前的實際值相同,但內存地址V中的變量已經經歷了A->B->A的改變。
可這樣的話看起來好像也沒毛病。
接下來咱們來結合實際的場景分析它:
咱們假設有一個CAS原理的ATM,小明有100元存款,要取錢50元。
因爲提款機硬件出了點小問題,提款操做被同時提交兩次,兩個線程都是獲取當前值100元,要更新成50元。理想狀況下,應該一個線程更新成功,另外一個線程更新失敗,小明的存款只被扣一次。
存款餘額:100元
ATM線程1:獲取當前值100,指望更新爲50
ATM線程2:獲取當前值100,指望更新爲50
線程1首先執行成功,把餘額從100改爲50。線程2由於某種緣由阻塞了。這時候,他的媽媽恰好給小明匯款50元。
存款餘額:50元
ATM線程1:獲取當前值100,成功更新爲50
ATM線程2:獲取當前值100,指望更新爲50,Block
線程3(他媽來存錢了):獲取當前值50,指望更新爲100
線程2仍然是阻塞狀態,線程3執行成功,把餘額從50改爲100。
存款餘額:100元
ATM線程1:獲取當前值100,成功更新爲50,已返回
ATM線程2:獲取當前值100,指望更新爲50,Block
線程3(他媽來存錢了):獲取當前值50,成功更新爲100
線程2恢復運行,因爲阻塞以前已經得到了「當前值」100,而且通過compare檢測,此時存款實際值也是100,因此成功把變量值100更新成了50。
存款餘額:50元
ATM線程1:獲取當前值100,成功更新爲50,已返回
ATM線程2:獲取「當前值」100,成功更新爲50
線程3(他媽來存錢了):獲取當前值50,成功更新爲100,已返回
這下問題就來了,小明的50元錢白白沒有了,本來線程2應當提交失敗,小灰的正確餘額應該保持爲100元,結果因爲ABA問題提交成功了。
思路和樂觀鎖差很少,採用版本號就好了,在compare階段不只要比較指望值A和地址V中的實際值,還要比較版本號是否一致。
咱們仍然以最初的例子來講明一下,假設地址V中存儲着變量值A,當前版本號是01。線程1得到了當前值A和版本號01,想要更新爲B,可是被阻塞了。
版本號01:內存地址V→A
線程1:獲取當前值A,版本號01,指望更新爲B
這時候發生ABA問題,內存中的值發生了屢次改變,最後仍然是A,版本號提高爲03
版本號03:內存地址V→A
線程1:獲取當前值A,版本號01,指望更新爲B
隨後線程1恢復運行,發現版本號不相等,因此更新失敗。
具體的能夠參考java中的AtomicStampedReference它用版本號比較作了CAS機制。
CAS原理差很少了,它雖然高效,可是有以下問題:
一、ABA問題,能夠經過版本號解決
二、循環時間長,開銷比較大:若是併發量至關高,CAS操做長時間不成功時,會致使其一直自旋,帶來CPU消耗比較大
補充一下自旋鎖和非自旋鎖的概念:
CAS做爲concurrent包基礎中的基礎,在打敗併發編程的旅途中有着舉足輕重的地位,接下來咱們將基於CAS講解,Concurrent包中最基礎的組件,咱們經常使用的ReetrantLock SemaPhore LinkedBlockingQueue ArrayBlockingQueue都是基於它實現的。它就是AQS。
傳送門:https://juejin.im/post/5c021b59f265da6175737f0b