一、什麼是CAS?html
CAS:Compare and Swap,即比較再交換。java
jdk5增長了併發包java.util.concurrent.*,其下面的類使用CAS算法實現了區別於synchronouse同步鎖的一種樂觀鎖。JDK 5以前Java語言是靠synchronized關鍵字保證同步的,這是一種獨佔鎖,也是是悲觀鎖。算法
二、CAS算法理解 數據庫
對CAS的理解,CAS是一種無鎖算法,CAS有3個操做數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。 編程
CAS比較與交換的僞代碼能夠表示爲:數組
do{緩存
備份舊數據;安全
基於舊數據構造新數據;數據結構
}多線程
while(!CAS( 內存地址,備份的舊數據,新數據 ))
注:t1,t2線程是同時更新同一變量56的值
由於t1和t2線程都同時去訪問同一變量56,因此他們會把主內存的值徹底拷貝一份到本身的工做內存空間,因此t1和t2線程的預期值都爲56。
假設t1在與t2線程競爭中線程t1能去更新變量的值,而其餘線程都失敗。(失敗的線程並不會被掛起,而是被告知此次競爭中失敗,並能夠再次發起嘗試)。t1線程去更新變量值改成57,而後寫到內存中。此時對於t2來講,內存值變爲了57,與預期值56不一致,就操做失敗了(想改的值再也不是原來的值)。
(上圖通俗的解釋是:CPU去更新一個值,但若是想改的值再也不是原來的值,操做就失敗,由於很明顯,有其它操做先改變了這個值。)
就是指當二者進行比較時,若是相等,則證實共享數據沒有被修改,替換成新值,而後繼續往下運行;若是不相等,說明共享數據已經被修改,放棄已經所作的操做,而後從新執行剛纔的操做。容易看出 CAS 操做是基於共享數據不會被修改的假設,採用了相似於數據庫的commit-retry 的模式。當同步衝突出現的機會不多時,這種假設能帶來較大的性能提高。
原子是世界上的最小單位,具備不可分割性。好比 a=0;(a非long和double類型) 這個操做是不可分割的,那麼咱們說這個操做時原子操做。再好比:a++; 這個操做實際是a = a + 1;是可分割的,因此他不是一個原子操做。非原子操做都會存在線程安全問題,須要咱們使用同步技術(sychronized)來讓它變成一個原子操做。一個操做是原子操做,那麼咱們稱它具備原子性。
java的concurrent包下提供了一些原子類,咱們能夠經過閱讀API來了解這些原子類的用法。好比:AtomicInteger、AtomicLong、AtomicReference等。
除了在i++操做時使用synchroinzed關鍵字實現同步外,還可使用AtomicInteger原子類進行實現
Java.util.concurrent.atomic 包中提供瞭如下原子類, 它們是線程安全的類
Java提供的原子類是靠 sun 基於 CAS 實現的,CAS 是一種樂觀鎖。參考:樂觀鎖與悲觀鎖
原子變量類至關於一種泛化的 volatile 變量,可以支持原子的和有條件的讀-改-寫操做。AtomicInteger 表示一個int類型的值,並提供了 get 和 set 方法,這些 Volatile 類型的int變量在讀取和寫入上有着相同的內存語義。它還提供了一個原子的 compareAndSet 方法(若是該方法成功執行,那麼將實現與讀取/寫入一個 volatile 變量相同的內存效果),以及原子的添加、遞增和遞減等方法。AtomicInteger 表面上很是像一個擴展的 Counter 類,但在發生競爭的狀況下能提供更高的可伸縮性,由於它直接利用了硬件對併發的支持。
接下來經過源代碼來看 AtomicInteger 具體是如何實現的原子操做。
首先看 value 的聲明:
private volatile int value;
volatile 修飾的 value 變量,保證了變量的可見性。
incrementAndGet() 方法,下面是具體的代碼:
public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }
經過源碼,能夠知道,這個方法的作法爲先獲取到當前的 value 屬性值,而後將 value 加 1,賦值給一個局部的 next 變量,然而,這兩步都是非線程安全的,可是內部有一個死循環,不斷去作 compareAndSet 操做,直到成功爲止,也就是修改的根本在 compareAndSet 方法裏面,compareAndSet()方法的代碼以下:
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
compareAndSet()方法調用的compareAndSwapInt()方法的聲明以下,是一個native方法。
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, intvar5);
compareAndSet 傳入的爲執行方法時獲取到的 value 屬性值,next 爲加 1 後的值, compareAndSet 所作的爲調用 Sun 的 UnSafe 的 compareAndSwapInt 方法來完成,此方法爲 native 方法,compareAndSwapInt 基於的是 CPU 的 CAS 指令來實現的。因此基於 CAS 的操做可認爲是無阻塞的,一個線程的失敗或掛起不會引發其它線程也失敗或掛起。而且因爲 CAS 操做是 CPU 原語,因此性能比較好。
相似的,還有 decrementAndGet() 方法。它和 incrementAndGet() 的區別是將 value 減 1,賦值給next 變量。
AtomicInteger 中還有 getAndIncrement() 和 getAndDecrement() 方法,他們的實現原理和上面的兩個方法徹底相同,區別是返回值不一樣,前兩個方法返回的是改變以後的值,即 next。而這兩個方法返回的是改變以前的值,即 current。還有不少的其餘方法,就不列舉了。
CAS(Compare-And-Swap)算法保證數據操做的原子性。
CAS 算法是硬件對於併發操做共享數據的支持。
CAS 包含了三個操做數:
內存值 V
預估值 A
更新值 B
當且僅當 V == A 時,V 將被賦值爲 B,不然什麼都不作,
固然若是須要的話,能夠設計成自旋鎖的模式,循環着不斷進行判斷 V 與 A 是否相等。
考慮以下問題:
關於CAS操做 提出問題:
狀況1.
兩個線程A和B同時對AtomicInteger(10)進行incrementAndGet()方法,都獲取到current = 10 ,compareAndSet比較時,內存總的值均未被修改, 那兩個線程都將執行了+1,那返回的結果應該都爲11吧?
狀況2
兩個線程A和B同時對AtomicInteger(10)進行incrementAndGet()方法,都獲取到current = 10 ,線程A線程先進行了compareAndSwapInt致使內存中的值變爲11,那線程B的在和內存中的值比較一直不相等, 那線程B不是死循環了嗎?
解決問題:
其實問題仍是在CAS上,內存值,預估值,更新值的問題
狀況1:不會存在返回結果都是 11 的狀況。原子類提供的就是原子操做,多線程狀況下不會存在數據不一致的狀況。具體緣由就是 CAS 操做,它會讀取內存和預期值(11)做比較,若是相同纔會進行賦值。
狀況2:同理。
其實你說的兩個線程「同時」,原子類的目的自己就是爲了不這種場景下的數據不一致,因此你說的這兩種狀況是不存在的。
固然若是使用繼承Thread類的方式實現多線程,那它的原子類變量是本身維護的,也就是線程獨立的,那就會存在問題。實現Runnable接口就不會存在這個問題,由於是資源共享的。
三、CAS開銷
前面說過了,CAS(比較並交換)是CPU指令級的操做,只有一步原子操做,因此很是快。並且CAS避免了請求操做系統來裁定鎖的問題,不用麻煩操做系統,直接在CPU內部就搞定了。但CAS就沒有開銷了嗎?不!有cache miss的狀況。這個問題比較複雜,首先須要瞭解CPU的硬件體系結構:
上圖能夠看到一個8核CPU計算機系統,每一個CPU有cache(CPU內部的高速緩存,寄存器),管芯內還帶有一個互聯模塊,使管芯內的兩個核能夠互相通訊。在圖中央的系統互聯模塊可讓四個管芯相互通訊,而且將管芯與主存鏈接起來。數據以「緩存線」爲單位在系統中傳輸,「緩存線」對應於內存中一個 2 的冪大小的字節塊,大小一般爲 32 到 256 字節之間。當 CPU 從內存中讀取一個變量到它的寄存器中時,必須首先將包含了該變量的緩存線讀取到 CPU 高速緩存。一樣地,CPU 將寄存器中的一個值存儲到內存時,不只必須將包含了該值的緩存線讀到 CPU 高速緩存,還必須確保沒有其餘 CPU 擁有該緩存線的拷貝。
好比,若是 CPU0 在對一個變量執行「比較並交換」(CAS)操做,而該變量所在的緩存線在 CPU7 的高速緩存中,就會發生如下通過簡化的事件序列:
CPU0 檢查本地高速緩存,沒有找到緩存線。
請求被轉發到 CPU0 和 CPU1 的互聯模塊,檢查 CPU1 的本地高速緩存,沒有找到緩存線。
請求被轉發到系統互聯模塊,檢查其餘三個管芯,得知緩存線被 CPU6和 CPU7 所在的管芯持有。
請求被轉發到 CPU6 和 CPU7 的互聯模塊,檢查這兩個 CPU 的高速緩存,在 CPU7 的高速緩存中找到緩存線。
CPU7 將緩存線發送給所屬的互聯模塊,而且刷新本身高速緩存中的緩存線。
CPU6 和 CPU7 的互聯模塊將緩存線發送給系統互聯模塊。
系統互聯模塊將緩存線發送給 CPU0 和 CPU1 的互聯模塊。
CPU0 和 CPU1 的互聯模塊將緩存線發送給 CPU0 的高速緩存。
CPU0 如今能夠對高速緩存中的變量執行 CAS 操做了
以上是刷新不一樣CPU緩存的開銷。最好狀況下的 CAS 操做消耗大概 40 納秒,超過 60 個時鐘週期。這裏的「最好狀況」是指對某一個變量執行 CAS 操做的 CPU 正好是最後一個操做該變量的CPU,因此對應的緩存線已經在 CPU 的高速緩存中了,相似地,最好狀況下的鎖操做(一個「round trip 對」包括獲取鎖和隨後的釋放鎖)消耗超過 60 納秒,超過 100 個時鐘週期。這裏的「最好狀況」意味着用於表示鎖的數據結構已經在獲取和釋放鎖的 CPU 所屬的高速緩存中了。鎖操做比 CAS 操做更加耗時,是因深刻理解並行編程
爲鎖操做的數據結構中須要兩個原子操做。緩存未命中消耗大概 140 納秒,超過 200 個時鐘週期。須要在存儲新值時查詢變量的舊值的 CAS 操做,消耗大概 300 納秒,超過 500 個時鐘週期。想一想這個,在執行一次 CAS 操做的時間裏,CPU 能夠執行 500 條普通指令。這代表了細粒度鎖的侷限性。
如下是cache miss cas 和lock的性能對比:
四、CAS算法在JDK中的應用
在原子類變量中,如java.util.concurrent.atomic中的AtomicXXX,都使用了這些底層的JVM支持爲數字類型的引用類型提供一種高效的CAS操做,而在java.util.concurrent中的大多數類在實現時都直接或間接的使用了這些原子變量類。
Java 1.7中AtomicInteger.incrementAndGet()的實現源碼爲:
因而可知,AtomicInteger.incrementAndGet的實現用了樂觀鎖技術,調用了類sun.misc.Unsafe庫裏面的 CAS算法,用CPU指令來實現無鎖自增。因此,AtomicInteger.incrementAndGet的自增比用synchronized的鎖效率倍增。
參考: java中的原子類