該系列文章已收錄在公眾號【Ccww技術博客】,原創技術文章第一時間推出
前言
在面試中,並發線程安全提問必然是不會缺少的,那基礎的CAS原理也必須了解,這樣在面試中才能加分,那來看看面試可能會問那些問題:
-
什么是樂觀鎖與悲觀鎖
-
什么樂觀鎖的實現方式-CAS(Compare and Swap),CAS(Compare and Swap)實現原理
-
在JDK並發包中的使用
-
CAS的缺陷
1. 什么是樂觀鎖與悲觀鎖?
總是假設最壞的情況,每次讀取數據的時候都默認其他線程會更改數據,因此需要進行加鎖操作,當其他線程想要訪問數據時,都需要阻塞掛起。悲觀鎖的實現:
-
傳統的關系型數據庫使用這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖;
-
Java里面的同步
synchronized
關鍵字的實現。
樂觀鎖
樂觀鎖,其實就是一種思想,總是認為不會產生並發問題,每次讀取數據的時候都認為其他線程不會修改數據,所以不上鎖,但是在更新的時候會判斷一下在此期間別的線程有沒有修改過數據,樂觀鎖適用於讀操作多的場景,這樣可以提高程序的吞吐量。實現方式:
-
CAS實現:Java中java.util.concurrent.atomic包下面的原子變量使用了樂觀鎖的一種CAS實現方式,CAS分析看下節。
-
版本號控制:一般是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛才讀取到的version值為當前數據庫中的version值相等時才更新,否則重試更新操作,直到更新成功
樂觀鎖適用於讀多寫少的情況下(多讀場景),悲觀鎖比較適用於寫多讀少場景
2. 樂觀鎖的實現方式-CAS(Compare and Swap),CAS(Compare and Swap)實現原理
背景
在jdk1.5之前都是使用synchronized
關鍵字保證同步,synchronized
保證了無論哪個線程持有共享變量的鎖,都會采用獨占的方式來訪問這些變量,導致會存在這些問題:
-
在多線程競爭下,加鎖、釋放鎖會導致較多的上下文切換和調度延時,引起性能問題
-
如果一個線程持有鎖,其他的線程就都會掛起,等待持有鎖的線程釋放鎖。
-
如果一個優先級高的線程等待一個優先級低的線程釋放鎖,會導致優先級倒置,引起性能風險
為了優化悲觀鎖這些問題,就出現了樂觀鎖:
假設沒有並發沖突,每次不加鎖操作同一變量,如果有並發沖突導致失敗,則重試直至成功。
CAS(Compare and Swap)原理
CAS 全稱是 compare and swap(比較並且交換),是一種用於在多線程環境下實現同步功能的機制,其也是無鎖優化,或者叫自旋,還有自適應自旋。
在jdk中,CAS
加volatile
關鍵字作為實現並發包的基石。沒有CAS就不會有並發包,java.util.concurrent中借助了CAS指令實現了一種區別於synchronized的一種樂觀鎖。
樂觀鎖的一種典型實現機制(CAS):
樂觀鎖主要就是兩個步驟:
-
沖突檢測
-
數據更新
當多個線程嘗試使用CAS同時更新同一個變量時,只有一個線程可以更新變量的值,其他的線程都會失敗,失敗的線程並不會掛起,而是告知這次競爭中失敗了,並可以再次嘗試。
在不使用鎖的情況下保證線程安全,CAS實現機制中有重要的三個操作數:
-
需要讀寫的內存位置(V)
-
預期原值(A)
-
新值(B)
首先先讀取需要讀寫的內存位置(V),然后比較需要讀寫的內存位置(V)和預期原值(A),如果內存位置與預期原值的A相匹配,那么將內存位置的值更新為新值B。如果內存位置與預期原值的值不匹配,那么處理器不會做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。具體可以分成三個步驟:
-
讀取(需要讀寫的內存位置(V))
-
比較(需要讀寫的內存位置(V)和預期原值(A))
-
寫回(新值(B))
3. CAS在JDK並發包中的使用
在JDK1.5以上 java.util.concurrent(JUC java並發工具包)是基於CAS算法實現的,相比於synchronized獨占鎖,堵塞算法,CAS是非堵塞算法的一種常見實現,使用樂觀鎖JUC在性能上有了很大的提升。
CAS如何在不使用鎖的情況下保證線程安全,看並發包中的原子操作類AtomicInteger::getAndIncrement()方法(相當於i++的操作):
// AtomicInteger中 //value的偏移量 private static final long valueOffset; //獲取值 private volatile int value; //設置value的偏移量 static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } //增加1 public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
-
首先value必須使用了volatile修飾,這就保證了他的可見性與有序性
-
需要初始化value的偏移量
-
unsafe.getAndAddInt通過偏移量進行CAS操作,每次從內存中讀取數據然后將數據進行+1操作,然后對原數據,+1后的結果進行CAS操作,成功的話返回結果,否則重試直到成功為止。
//unsafe中 public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { //使用偏移量獲取內存中value值 var5 = this.getIntVolatile(var1, var2); //比較並value加+1 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
JAVA實現CAS的原理,unsafe::compareAndSwapInt是借助C來調用CPU底層指令實現的。下面是sun.misc.Unsafe::compareAndSwapInt()方法的源代碼:
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
4. CAS的缺陷
ABA問題
在多線程場景下CAS會出現ABA問題,例如有2個線程同時對同一個值(初始值為A)進行CAS操作,這三個線程如下
線程1,期望值為A,欲更新的值為B
線程2,期望值為A,欲更新的值為B
線程3,期望值為B,欲更新的值為A
-
線程1搶先獲得CPU時間片,而線程2因為其他原因阻塞了,線程1取值與期望的A值比較,發現相等然后將值更新為B,
-
這個時候出現了線程3,線程3取值與期望的值B比較,發現相等則將值更新為A
-
此時線程2從阻塞中恢復,並且獲得了CPU時間片,這時候線程2取值與期望的值A比較,發現相等則將值更新為B,雖然線程2也完成了操作,但是線程2並不知道值已經經過了A->B->A的變化過程。
ABA問題帶來的危害:
小明在提款機,提取了50元,因為提款機問題,有兩個線程,同時把余額從100變為50
線程1(提款機):獲取當前值100,期望更新為50,
線程2(提款機):獲取當前值100,期望更新為50,
線程1成功執行,線程2某種原因block了,這時,某人給小明匯款50 線程3(默認):獲取當前值50,期望更新為100,
這時候線程3成功執行,余額變為100, 線程2從Block中恢復,獲取到的也是100,compare之后,繼續更新余額為50!!!
此時可以看到,實際余額應該為100(100-50+50),但是實際上變為了50(100-50+50-50)這就是ABA問題帶來的成功提交。
解決方法
-
AtomicStampedReference: 帶有時間戳的對象引用來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標志是否等於預期標志,如果全部相等,則以原子方式將該引用和該標志的值設置為給定的更新值。
-
public boolean compareAndSet( V expectedReference,//預期引用 V newReference,//更新后的引用 int expectedStamp, //預期標志 int newStamp //更新后的標志 )
-
version:在變量前面加上版本號,每次變量更新的時候變量的版本號都+1,即A->B->A就變成了1A->2B->3A
循環時間長開銷大
自旋CAS(不成功,就一直循環執行,直到成功)如果長時間不成功,會給CPU帶來極大的執行開銷。
解決方法:
-
限制自旋次數,防止進入死循環
-
JVM能支持處理器提供的pause指令那么效率會有一定的提升,
只能保證一個共享變量的原子操作
當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性
解決方法:
-
如果需要對多個共享變量進行操作,可以使用加鎖方式(悲觀鎖)保證原子性,
-
可以把多個共享變量合並成一個共享變量進行CAS操作。
該系列文章已收錄在公眾號【Ccww技術博客】,原創技術文章第一時間推出