前面咱們屢次提到一個累加器的例子,示例代碼以下。在這個例子中,add10K() 這個方法不是線程安全的,問題就出在變量 count 的可見性和 count+=1 的原子性上。可見性問題能夠用 volatile 來解決,而原子性問題咱們前面一直都是採用的互斥鎖方案。數組
public class Test { long count = 0; void add10K() { int idx = 0; while(idx++ < 10000) { count += 1; } } }
其實對於簡單的原子性問題,還有一種無鎖
方案。Java SDK 併發包將這種無鎖方案封裝提煉以後,實現了一系列的原子類。安全
在下面的代碼中,咱們將原來的 long 型變量 count 替換爲了原子類 AtomicLong,原來的count +=1
替換成了 count.getAndIncrement(),僅須要這兩處簡單的改動就能使 add10K() 方法變成線程安全的,原子類的使用仍是挺簡單的。併發
public class Test { AtomicLong count = new AtomicLong(0); void add10K() { int idx = 0; while(idx++ < 10000) { count.getAndIncrement(); } } }
無鎖方案相對互斥鎖方案,最大的好處就是性能
。互斥鎖方案爲了保證互斥性,須要執行加鎖、解鎖操做,而加鎖、解鎖操做自己就消耗性能;同時拿不到鎖的線程還會進入阻塞狀態,進而觸發線程切換,線程切換對性能的消耗也很大。 相比之下,無鎖方案則徹底沒有加鎖、解鎖的性能消耗,同時還能保證互斥性,既解決了問題,又沒有帶來新的問題,可謂絕佳方案。那它是如何作到的呢?函數
其實原子類性能高的祕密很簡單,硬件支持而已。CPU 爲了解決併發問題,提供了 CAS 指令(CAS,全稱是 Compare And Swap,即「比較並交換」)。CAS 指令包含 3 個參數:共享變量的內存地址 A、用於比較的值 B 和共享變量的新值 C;而且只有當內存中地址 A 處的值等於 B 時,才能將內存中地址 A 處的值更新爲新值 C。做爲一條 CPU 指令,CAS 指令自己是可以保證原子性的
。性能
你能夠經過下面 CAS 指令的模擬代碼來理解 CAS 的工做原理。在下面的模擬程序中有兩個參數,一個是指望值 expect,另外一個是須要寫入的新值 newValue, 只有當目前 count 的值和指望值 expect 相等時,纔會將 count 更新爲 newValue
this
class SimulatedCAS{ int count; synchronized int cas( int expect, int newValue){ // 讀目前 count 的值 int curValue = count; // 比較目前 count 值是否 == 指望值 if(curValue == expect){ // 若是是,則更新 count 的值 count = newValue; } // 返回寫入前的值 return curValue; } }
count += 1
的一個核心問題是:基於內存中 count 的當前值 A 計算出來的 count+=1 爲 A+1,在將 A+1 寫入內存的時候,極可能此時內存中 count 已經被其餘線程更新過了,這樣就會致使錯誤地覆蓋其餘線程寫入的值。spa
使用 CAS 來解決併發問題,通常都會伴隨着自旋,而所謂自旋,其實就是循環嘗試。例如,實現一個線程安全的count += 1
操做,CAS+ 自旋
的實現方案以下所示,首先計算 newValue = count+1,若是 cas(count,newValue) 返回的值不等於 count,則意味着線程在執行完代碼①處以後,執行代碼②處以前,count 的值被其餘線程更新過。那此時該怎麼處理呢?能夠採用自旋方案,就像下面代碼中展現的,能夠從新讀 count 最新的值來計算 newValue 並嘗試再次更新,直到成功。線程
class SimulatedCAS{ volatile int count; // 實現 count+=1 addOne(){ do { newValue = count+1; //① }while(count != cas(count,newValue) //② } // 模擬實現 CAS,僅用來幫助理解 synchronized int cas( int expect, int newValue){ // 讀目前 count 的值 int curValue = count; // 比較目前 count 值是否 == 指望值 if(curValue == expect){ // 若是是,則更新 count 的值 count= newValue; } // 返回寫入前的值 return curValue; } }
經過上面的示例代碼,想必你已經發現了,CAS 這種無鎖方案,徹底沒有加鎖、解鎖操做,即使兩個線程徹底同時執行 addOne() 方法,也不會有線程被阻塞,因此相對於互斥鎖方案來講,性能好了不少。code
可是在 CAS 方案中,有一個問題可能會常被你忽略,那就是ABA
問題。對象
前面咱們提到「若是 cas(count,newValue) 返回的值不等於count
,意味着線程在執行完代碼①處以後,執行代碼②處以前,count 的值被其餘線程更新過
。那若是 cas(count,newValue) 返回的值等於count
,是否就可以認爲 count 的值沒有被其餘線程更新過
呢?
顯然不是的,假設 count 本來是 A,線程 T1 在執行完代碼①處以後,執行代碼②處以前,有可能 count 被線程 T2 更新成了 B,以後又被 T3 更新回了 A,這樣線程 T1 雖然看到的一直是 A,可是其實已經被其餘線程更新過了,這就是 ABA 問題。
可能大多數狀況下咱們並不關心 ABA 問題,例如數值的原子遞增,但也不能全部狀況下都不關心,例如原子化的更新對象極可能就須要關心 ABA 問題,由於兩個 A 雖然相等,可是第二個 A 的屬性可能已經發生變化了。因此在使用 CAS 方案的時候,必定要先 check 一下。
在本文開始部分,咱們使用原子類 AtomicLong 的 getAndIncrement() 方法替代了count+1
1,從而實現了線程安全。原子類 AtomicLong 的 getAndIncrement() 方法內部就是基於 CAS 實現的,下面咱們來看看 Java 是如何使用 CAS 來實現原子化的
在 Java 1.8 版本中,getAndIncrement() 方法會轉調 unsafe.getAndAddLong() 方法。這裏 this 和 valueOffset 兩個參數能夠惟一肯定共享變量的內存地址。
final long getAndIncrement() { return unsafe.getAndAddLong( this, valueOffset, 1L); }
unsafe.getAndAddLong() 方法的源碼以下,該方法首先會在內存中讀取共享變量的值,以後循環調用 compareAndSwapLong() 方法來嘗試設置共享變量的值,直到成功爲止。compareAndSwapLong() 是一個 native 方法,只有當內存中共享變量的值等於 expected 時,纔會將共享變量的值更新爲 x,而且返回 true;不然返回 fasle。compareAndSwapLong 的語義和 CAS 指令的語義的差異僅僅是返回值不一樣而已。
public final long getAndAddLong( Object o, long offset, long delta){ long v; do { // 讀取內存中的值 v = getLongVolatile(o, offset); } while (!compareAndSwapLong( o, offset, v, v + delta)); return v; } // 原子性地將變量更新爲 x // 條件是內存中的值等於 expected // 更新成功則返回 true native boolean compareAndSwapLong( Object o, long offset, long expected, long x);
另外,須要你注意的是,getAndAddLong() 方法的實現,基本上就是 CAS 使用的經典範例。因此請你再次體會下面這段抽象後的代碼片斷,它在不少無鎖程序中常常出現。Java 提供的原子類裏面 CAS 通常被實現爲 compareAndSet(),compareAndSet() 的語義和 CAS 指令的語義的差異僅僅是返回值不一樣而已,compareAndSet() 裏面若是更新成功,則會返回 true,不然返回 false。
do { // 獲取當前值 oldV = xxxx; // 根據當前值計算新值 newV = ...oldV... }while(!compareAndSet(oldV,newV);
Java SDK 併發包裏提供的原子類內容很豐富,咱們能夠將它們分爲五個類別:
。這五個類別提供的方法基本上是類似的,而且每一個類別都有若干原子類,你能夠經過下面的原子類組成概覽圖來得到一個全局的印象。下面咱們詳細解讀這五個類別。
相關實現有 AtomicBoolean、AtomicInteger 和 AtomicLong,提供的方法主要有如下這些,詳情你能夠參考 SDK 的源代碼,都很簡單,這裏就不詳細介紹了。
getAndIncrement() // 原子化 i++ getAndDecrement() // 原子化的 i-- incrementAndGet() // 原子化的 ++i decrementAndGet() // 原子化的 --i // 當前值 +=delta,返回 += 前的值 getAndAdd(delta) // 當前值 +=delta,返回 += 後的值 addAndGet(delta) //CAS 操做,返回是否成功 compareAndSet(expect, update) // 如下四個方法 // 新值能夠經過傳入 func 函數來計算 getAndUpdate(func) updateAndGet(func) getAndAccumulate(x,func) accumulateAndGet(x,func)
相關實現有 AtomicReference、AtomicStampedReference 和 AtomicMarkableReference,利用它們能夠實現對象引用的原子化更新。AtomicReference 提供的方法和原子化的基本數據類型差很少,這裏再也不贅述。不過須要注意的是,對象引用的更新須要重點關注 ABA 問題,AtomicStampedReference 和 AtomicMarkableReference 這兩個原子類能夠解決 ABA 問題。
解決 ABA 問題的思路其實很簡單,增長一個版本號維度就能夠了。每次執行 CAS 操做,附加再更新一個版本號,只要保證版本號是遞增的,那麼即使 A 變成 B 以後再變回 A,版本號也不會變回來(版本號遞增的)。AtomicStampedReference 實現的 CAS 方法就增長了版本號參數,方法簽名以下:
boolean compareAndSet( V expectedReference, V newReference, int expectedStamp, int newStamp)
AtomicMarkableReference 的實現機制則更簡單,將版本號簡化成了一個 Boolean 值,方法簽名以下:
boolean compareAndSet( V expectedReference, V newReference, boolean expectedMark, boolean newMark)
相關實現有 AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray,利用這些原子類,咱們能夠原子化地更新數組裏面的每個元素。這些類提供的方法和原子化的基本數據類型的區別僅僅是:每一個方法多了一個數組的索引參數,因此這裏也再也不贅述了。
相關實現有 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater,利用它們能夠原子化地更新對象的屬性,這三個方法都是利用反射機制實現的,建立更新器的方法以下:
public static <U> AtomicXXXFieldUpdater<U> newUpdater(Class<U> tclass, String fieldName)
須要注意的是,對象屬性必須是 volatile 類型的,只有這樣才能保證可見性
;若是對象屬性不是 volatile 類型的,newUpdater() 方法會拋出 IllegalArgumentException 這個運行時異常。
你會發現 newUpdater() 的方法參數只有類的信息,沒有對象的引用,而更新對象
的屬性,必定須要對象的引用,那這個參數是在哪裏傳入的呢?是在原子操做的方法參數中傳入的。例如 compareAndSet() 這個原子操做,相比原子化的基本數據類型多了一個對象引用 obj。原子化對象屬性更新器相關的方法,相比原子化的基本數據類型僅僅是多了對象引用參數,因此這裏也再也不贅述了。
boolean compareAndSet( T obj, int expect, int update)
DoubleAccumulator、DoubleAdder、LongAccumulator 和 LongAdder,這四個類僅僅用來執行累加操做,相比原子化的基本數據類型,速度更快,可是不支持 compareAndSet() 方法。若是你僅僅須要累加操做,使用原子化的累加器性能會更好。
無鎖方案相對於互斥鎖方案,優勢很是多,首先性能好,其次是基本不會出現死鎖問題(但可能出現飢餓和活鎖問題,由於自旋會反覆重試)。Java 提供的原子類大部分都實現了 compareAndSet() 方法。
Java 提供的原子類可以解決一些簡單的原子性問題,但你可能會發現,上面咱們全部原子類的方法都是針對一個共享變量的,若是你須要解決多個變量的原子性問題,建議仍是使用互斥鎖方案。原子類雖好,但使用要慎之又慎。