朋友,文章優先發布在公衆號上,若是你願意,能夠掃右側二維碼支持一下下~,謝謝!java
平常編碼過程當中,基本不會直接用到 CAS 操做,都是經過一些JDK 封裝好的併發工具類來使用的,在 java.util.concurrent 包下。面試
可是面試時 CAS 仍是個高頻考點,因此呀,你還不得不硬着頭皮去死磕一下這塊的技能點,總比一問三不知強吧?算法
通常都是先針對一些簡單的併發知識問起,還有的面試官,比較直接:數組
面試官:Java併發工具類中的 CAS 機制講一講?緩存
小東:額?大腦中問本身「啥是 CAS?」我聽過的,容我想想...安全
一分鐘過去了...多線程
小東:嘿嘿~,這塊我看過的,記不大清楚了。架構
面試官:好的,今天先到這吧~併發
小東:在路上函數
固然 CAS 你若真不懂,你能夠引導面試官到你擅長的技術點上,用你的其餘技能亮點扳回一局。
接下來,咱們經過一個示例代碼來講:
// 類的成員變量 static int data = 0; // main方法內代碼 IntStream.range(0, 2).forEach((i) -> { new Thread(() -> { try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } IntStream.range(0, 100).forEach(y -> { data++; }); }).start(); }); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(data); }
結合圖示理解:
上述代碼,問題很明顯,data 是類中的成員變量,int 類型,即共享的資源。當多個線程同時
執行 data++
操做時,結果可能不等於 200,爲了模擬出效果,線程中 sleep 了 20 毫秒,讓線程就緒,代碼運行屢次,結果都不是 200 。
示例代碼執行結果代表了,多個線程同時操做共享變量致使告終果不許確,線程是不安全的。如何解決呢?
方案一:使用 synchronized 關鍵字
使用 synchronized 關鍵字,線程內使用同步代碼塊,由JVM自身的機制來保障線程的安全性。
synchronized 關鍵代碼:
// 類中定義的Object鎖對象 Object lock = new Object(); // synchronized 同步塊 () 中使用 lock 對象鎖定資源 IntStream.range(0, 100).forEach(y -> { synchronized (lock.getClass()) { data++; } });
方案二:使用 Lock 鎖
高併發場景下,使用 Lock 鎖要比使用 synchronized 關鍵字,在性能上獲得極大的提升。
由於 Lock 底層是經過 AQS + CAS 機制來實現的。關於 AQS 機制能夠參見往期文章 <<經過經過一個生活中的案例場景,揭開併發包底層AQS的神祕面紗>> 。CAS 機制會在文章中下面講到。
使用 Lock 的關鍵代碼:
// 類中定義成員變量 Lock lock = new ReentrantLock(); // 執行 lock() 方法加鎖,執行 unlock() 方法解鎖 IntStream.range(0, 100).forEach(y -> { lock.lock(); data++; lock.unlock(); });
結合圖示理解:
方案三:使用 Atomic 原子類
除上面兩種方案還有沒有更爲優雅的方案?synchronized 的使用在 JDK1.6 版本之後作了不少優化,若是併發量不大,相比 Lock 更爲安全,性能也能接受,因其得益於 JVM 底層機制來保障,自動釋放鎖,無需硬編碼方式釋放鎖。而使用 Lock 方式,一旦 unlock() 方法使用不規範,可能致使死鎖。
JDK 併發包全部的原子類以下所示:
使用 AtomicInteger 工具類實現代碼:
// 類中成員變量定義原子類 AtomicInteger atomicData = new AtomicInteger(); // 代碼中原子類的使用方式 IntStream.range(0, 2).forEach((i) -> { new Thread(() -> { try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } IntStream.range(0, 100).forEach(y -> { // 原子類自增 atomicData.incrementAndGet(); }); }).start(); }); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } // 經過 get () 方法獲取結果 System.out.println(atomicData.get());
結合圖示理解:
之因此推薦使用 Atomic 原子類,由於其底層基於 CAS 樂觀鎖來實現的,下文會詳細分析。
方案四:使用 LongAdder 原子類
LongAdder 原子類在 JDK1.8 中新增的類, 跟方案三中提到的 AtomicInteger 相似,都是在 java.util.concurrent.atomic 併發包下的。
LongAdder 適合於高併發場景下,特別是寫大於讀的場景,相較於 AtomicInteger、AtomicLong 性能更好,代價是消耗更多的空間,以空間換時間。
使用 LongAdder 工具類實現代碼:
// 類中成員變量定義的LongAdder LongAdder longAdderData = new LongAdder(); // 代碼中原子類的使用方式 IntStream.range(0, 2).forEach((i) -> { new Thread(() -> { try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } IntStream.range(0, 100).forEach(y -> { // 使用 increment() 方法自增 longAdderData.increment(); }); }).start(); }); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } // 使用 sum() 獲取結果 System.out.println(longAdderData.sum());
結合圖示理解:
可是,若是使用了 LongAdder 原子類,固然其底層也是基於 CAS 機制實現的。LongAdder 內部維護了 base 變量和 Cell[] 數組,當多線程併發寫的狀況下,各個線程都在寫入本身的 Cell 中,LongAdder 操做後返回的是個近似準確的值,最終也會返回一個準確的值。
換句話說,使用了 LongAdder 後獲取的結果並非實時的,對實時性要求高的仍是建議使用其餘的原子類,如 AtomicInteger 等。
volatile 關鍵字方案?
可能還有朋友會說,還想到另一種方案:使用** volatile
** 關鍵字啊。
通過驗證,是不可行的,你們能夠試試,就本文給出的示例代碼直接執行,結果都不等於 200,說明線程仍然是不安全的。
data++ 自增賦值並非原子的,跟 Java內存模型有關。
在非線程安全的圖示中有標註執行線程本地,會有個內存副本,即本地的工做內存,實際執行過程會通過以下幾個步驟:
(1)執行線程從本地工做內存讀取 data,若是有值直接獲取,若是沒有值,會從主內存讀取,而後將其放到本地工做內存當中。
(2)執行線程在本地工做內存中執行 +1 操做。
(3)將 data 的值寫入主內存。
結論:請記住!
一個變量簡單的讀取和賦值操做是原子性的,將一個變量賦值給另一個變量不是原子性的。
Java內存模型(JMM)僅僅保障了變量的基本讀取和賦值操做是原子性的,其餘均不會保證的。若是想要使某段代碼塊要求具有原子性,就須要使用 synchronized 關鍵字、併發包中的 Lock 鎖、併發包中 Atomic 各類類型的原子類來實現,即上面咱們提到的四種方案都是可行的。
而 volatile
關鍵字修飾的變量,偏偏是不能保障原子性的,僅能保障可見性和有序性。
CAS 被認爲是一種樂觀鎖,有樂觀鎖,相對應的是悲觀鎖。
在上述示例中,咱們使用了 synchronized,若是在線程競爭壓力大的狀況下,synchronized 內部會升級爲重量級鎖,此時僅能有一個線程進入代碼塊執行,若是這把鎖始終不能釋放,其餘線程會一直阻塞等待下去。此時,能夠認爲是悲觀鎖。
悲觀鎖會因線程一直阻塞致使系統上下文切換,系統的性能開銷大。
那麼,咱們能夠用樂觀鎖來解決,所謂的樂觀鎖,其實就是一種思想。
樂觀鎖,會以一種更加樂觀的態度對待事情,認爲本身能夠操做成功。當多個線程操做同一個共享資源時,僅能有一個線程同一時間得到鎖成功,在樂觀鎖中,其餘線程發現本身沒法成功得到鎖,並不會像悲觀鎖那樣阻塞線程,而是直接返回,能夠去選擇再次重試得到鎖,也能夠直接退出。
CAS 正是樂觀鎖的核心算法實現。
在示例代碼的方案中都提到了 AtomicInteger、LongAdder、Lock鎖底層,此外,固然還包括 java.util.concurrent.atomic 併發包下的全部原子類都是基於 CAS 來實現的。
以 AtomicInteger 原子整型類爲例,一塊兒來分析下 CAS 底層實現機制。
atomicData.incrementAndGet()
源碼以下所示:
// 提供自增易用的方法,返回增長1後的值 public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } // 額外提供的compareAndSet方法 public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } // Unsafe 類的提供的方法 public final int getAndAddInt (Object o,long offset, int delta){ int v; do { v = getIntVolatile(o, offset); } while (!weakCompareAndSetInt(o, offset, v, v + delta)); return v; }
咱們看到了 AtomicInteger 內部方法都是基於 Unsafe 類實現的,Unsafe 類是個跟底層硬件CPU指令通信的複製工具類。
由這段代碼看到:
unsafe.compareAndSwapInt(this, valueOffset, expect, update)
所謂的 CAS,實際上是個簡稱,全稱是 Compare And Swap,對比以後交換數據。
上面的方法,有幾個重要的參數:
(1)this,Unsafe 對象自己,須要經過這個類來獲取 value 的內存偏移地址。
(2)valueOffset,value 變量的內存偏移地址。
(3)expect,指望更新的值。
(4)update,要更新的最新值。
若是原子變量中的 value 值等於 expect,則使用 update 值更新該值並返回 true,不然返回 false。
再看如何得到 valueOffset的:
// Unsafe實例 private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { // 得到value在AtomicInteger中的偏移量 valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } // 實際變量的值 private volatile int value;
這裏看到了 value 實際的變量,是由 volatile 關鍵字修飾的,爲了保證在多線程下的內存可見性。
爲什麼能經過 Unsafe.getUnsafe() 方法能得到 Unsafe 類的實例?其實由於 AtomicInteger 類也在 **rt.jar **包下面的,因此 AtomicInteger 類就是經過 Bootstrap 根類加載器進行加載的。
源碼以下所示:
@CallerSensitive public static Unsafe getUnsafe() { Class var0 = Reflection.getCallerClass(); // Bootstrap 類加載器是C++的,正常返回null,不然就拋異常。 if (!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException("Unsafe"); } else { return theUnsafe; } }
類加載器委託關係:
CPU 處理器速度遠遠大於在主內存中的,爲了解決速度差別,在他們之間架設了多級緩存,如 L一、L二、L3 級別的緩存,這些緩存離CPU越近就越快,將頻繁操做的數據緩存到這裏,加快訪問速度 ,以下圖所示:
如今都是多核 CPU 處理器,每一個 CPU 處理器內維護了一塊字節的內存,每一個內核內部維護着一塊字節的緩存,當多線程併發讀寫時,就會出現緩存數據不一致的狀況。
此時,處理器提供:
當一個處理器要操做共享變量時,在 BUS 總線上發出一個 Lock 信號,其餘處理就沒法操做這個共享變量了。
缺點很明顯,總線鎖定在阻塞其它處理器獲取該共享變量的操做請求時,也可能會致使大量阻塞,從而增長系統的性能開銷。
後來的處理器都提供了緩存鎖定機制,也就說當某個處理器對緩存中的共享變量進行了操做,其餘處理器會有個嗅探機制,將其餘處理器的該共享變量的緩存失效,待其餘線程讀取時會從新從主內存中讀取最新的數據,基於 MESI 緩存一致性協議來實現的。
現代的處理器基本都支持和使用的緩存鎖定機制。
注意:
有以下兩種狀況處理器不會使用緩存鎖定:
(1)當操做的數據跨多個緩存行,或沒被緩存在處理器內部,則處理器會使用總線鎖定。
(2)有些處理器不支持緩存鎖定,好比:Intel 486 和 Pentium 處理器也會調用總線鎖定。
其實,掌握以上內容,對於 CAS 機制的理解相對來講算是比較清楚了。
固然,若是感興趣,也能夠繼續深刻學習用到了哪些硬件 CPU 指令。
底層硬件經過將 CAS 裏的多個操做在硬件層面語義實現上,經過一條處理器指令保證了原子性操做。這些指令以下所示:
(1)測試並設置(Tetst-and-Set)
(2)獲取並增長(Fetch-and-Increment)
(3)交換(Swap)
(4)比較並交換(Compare-and-Swap)
(5)加載連接/條件存儲(Load-Linked/Store-Conditional)
前面三條大部分處理器已經實現,後面的兩條是現代處理器當中新增長的。並且根據不一樣的體系結構,指令存在着明顯差別。
在IA64,x86 指令集中有 cmpxchg 指令完成 CAS 功能,在 sparc-TSO 也有 casa 指令實現,而在 ARM 和 PowerPC 架構下,則須要使用一對 ldrex/strex 指令來完成 LL/SC 的功能。在精簡指令集的體系架構中,則一般是靠一對兒指令,如:load and reserve 和 **store conditional ** 實現的,在大多數處理器上 CAS 都是個很是輕量級的操做,這也是其優點所在。
sun.misc.Unsafe 中 CAS 的核心方法:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
這三個方法能夠對應去查看 openjdk 的 hotspot 源碼:
源碼位置:hotspot/src/share/vm/prims/unsafe.cpp
#define FN_PTR(f) CAST_FROM_FN_PTR(void*, &f) {CC"compareAndSwapObject", CC"("OBJ"J"OBJ""OBJ")Z", FN_PTR(Unsafe_CompareAndSwapObject)}, {CC"compareAndSwapInt", CC"("OBJ"J""I""I"")Z", FN_PTR(Unsafe_CompareAndSwapInt)}, {CC"compareAndSwapLong", CC"("OBJ"J""J""J"")Z", FN_PTR(Unsafe_CompareAndSwapLong)},
上述三個方法,最終在 hotspot 源碼實現中都會調用統一的 cmpxchg 函數,能夠在 hotspot 源碼中找到核心代碼。
源碼地址:hotspot/src/share/vm/runtime/Atomic.cpp
cmpxchg 函數源碼:
jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte*dest, jbyte compare_value) { assert (sizeof(jbyte) == 1,"assumption."); uintptr_t dest_addr = (uintptr_t) dest; uintptr_t offset = dest_addr % sizeof(jint); volatile jint*dest_int = ( volatile jint*)(dest_addr - offset); // 對象當前值 jint cur = *dest_int; // 當前值cur的地址 jbyte * cur_as_bytes = (jbyte *) ( & cur); // new_val地址 jint new_val = cur; jbyte * new_val_as_bytes = (jbyte *) ( & new_val); // new_val存exchange_value,後面修改則直接從new_val中取值 new_val_as_bytes[offset] = exchange_value; // 比較當前值與指望值,若是相同則更新,不一樣則直接返回 while (cur_as_bytes[offset] == compare_value) { // 調用匯編指令cmpxchg執行CAS操做,指望值爲cur,更新值爲new_val jint res = cmpxchg(new_val, dest_int, cur); if (res == cur) break; cur = res; new_val = cur; new_val_as_bytes[offset] = exchange_value; } // 返回當前值 return cur_as_bytes[offset]; }
源碼中具體變量添加了註釋,由於都是 C++ 代碼,因此做爲了解便可 ~
jint res = cmpxchg(new_val, dest_int, cur);
這裏就是調用了彙編指令 cmpxchg 了,其中也是包含了三個參數,跟CAS上的參數能對應上。
任何技術都要找到適合的場景,都不是萬能的,CAS 機制也同樣,也有反作用。
問題1:
做爲樂觀鎖的一種實現,當多線程競爭資源激烈的狀況下,並且鎖定的資源處理耗時,那麼其餘線程就要考慮自旋的次數限制,避免過分的消耗 CPU。
另外,能夠考慮上文示例代碼中提到的 LongAdder 來解決,LongAdder 以空間換時間的方式,來解決 CAS 大量失敗後長時間佔用 CPU 資源,加大了系統性能開銷的問題。
問題2:
A-->B--->A 問題,假設有一個變量 A ,修改成B,而後又修改成了 A,實際已經修改過了,但 CAS 可能沒法感知,形成了不合理的值修改操做。
整數類型還好,若是是對象引用類型,包含了多個變量,那怎麼辦?加個版本號或時間戳唄,沒問題!
JDK 中 java.util.concurrent.atomic 併發包下,提供了 AtomicStampedReference,經過爲引用創建個 Stamp 相似版本號的方式,確保 CAS 操做的正確性。
但願此文你們收藏消化,CAS 在JDK併發包底層實現中是個很是重要的算法。
撰文不易,文章中有什麼問題還請指正!
歡迎關注個人公衆號,掃二維碼關注得到更多精彩文章,與你一同成長~