前言
為保證計數器中 count=+1 的原子性,我們在前面使用的都是 synchronized 互斥鎖方案,加鎖獨佔訪問的方式未免太過霸道,於是我們來介紹另一種解決原子性問題的無鎖方案:原子變量。在正式介紹原子變量之前,我們先來總結下鎖的不足,然後深入介紹原子變量。
鎖的劣勢
通過對共享變量加鎖,使得獲取到鎖的線程可以採用獨佔方式來訪問共享變量,並且對變量的修改對隨後獲取這個鎖的其他線程都是可見的(Happens-Before規則)。
當多個線程同時請求鎖時,對於沒有獲取到鎖的線程將有可能被掛起並且在稍後才恢復運行(有時會選擇自旋等待)。當線程恢復執行時,必須等待其他線程執行完它們的時間片後,才能被調度執行。我們需要知道,在掛起和恢復線程等過程中會存在著很大的開銷,並且通常存在著較長時間的中斷。
加鎖方案在競爭激烈的情況下,其性能會由於上下文切換的開銷和調度延遲而降低。而如果鎖的持有時間非常短,那麼在不恰當的時間請求鎖時,線程休眠的代價將會不太划算。
加鎖還存在一些缺點:
當一個線程正在等待鎖時,它不能做任何其他事情。如果一個線程在持有鎖的情況下被延遲執行(例如發生了缺頁錯誤、調度延遲、或者其他情況),那麼所有需要這個鎖的線程都無法執行下去。如果被阻塞的線程優先級較高,而持有鎖的線程優先級較低,那麼將會導致優先級反轉問題。即使高優先級的線程可以搶先執行,但仍然需要等待鎖被釋放,從而導致它的優先級會降到低優先級線程的級別。如果持有鎖的線程被永久地阻塞(例如優於出現了無限循環,死鎖,活鎖或者其他活躍性問題),所有等待這個鎖的線程就永遠無法執行下去。與鎖相比,valatile變量是一種輕量級地同步機制,因為在使用這些變量時不會發生上下文切換或線程調度等操作。與鎖一樣雖然提供了可見性保證,但是volatile變量不能用於構建原子的複合操作。好消息是,下面我們將介紹的原子變量不僅提供了與volatile變量相同的內存語義,還支持原子的更新操作,比基於鎖的方案有著更高的可伸縮性。
原子變量的實現原理: 硬體對並發的支持
硬體對並發的支持
獨佔鎖是一項悲觀技術,對於細粒度操作(例如計數器),還有另外一種更高效的方法,也是樂觀方法,通過這種方法也可以在不受其他線程幹擾的情況下完成更新操作。這種方法通過藉助衝突檢查機制來判斷在更新過程中是否存在來自其他線程的幹擾,如果存在,這個操作將失敗,並且可以重新嘗試(也可以不嘗試)。
這種方法也就是處理器中提供的一些特殊指令(這些特殊指令本身可以保證原子性)。這些指令用於管理對共享數據的並發訪問。在早期的處理器中支持原子的測試並設置(Test-and-Set),獲取並遞增(Fetch-and-Increment)以及交換(Swap)等指令,這些指令足以實現各種互斥體,而這些互斥體又可以實現一些更加複雜的並發對象。現在幾乎所有的現代處理器都包含了某種形式的原子讀-改-寫指令,例如比較並交換(CAS,Compare-and-Swap)或者關聯加載/條件存儲(Load-Linked/Store-Conditional)。
作業系統和JVM使用這些指令來實現鎖和並發的數據結構,在Java 5.0之前,Java類中還不能直接使用這些指令。
原子類由這些特殊的指令實現,所有其性能會比較高。
比較並交換
在大多數處理架構中,對上述方法的實現是實現一個比較並交換(CAS)之指令。(在其他處理器中,採用一對指令來實現相同的功能:關聯加載與條件存儲。)
CAS包含了3個操作數:需要讀寫的內存地址V,進行比較的值A和擬寫入的新值B。
若且唯若V中的值等於A時,CAS才會通過原子方式用新值B來更新V中的值,否則不會執行任何操作。無論位置V的值是否等於A,都將返回V原有的值。
CAS是一項樂觀的技術,它希望能成功地執行更新操作,並且如果有另一個線程在最近一次檢查後更新了該變量,那麼CAS能檢測到這個錯誤。
以下的SimulatedCAS模擬代碼說明CAS的語義,用來理解CAS的工作原理。
public class SimulatedCAS {private int value;public synchronized int get() {return value;}public synchronized int compareAndSwap(int expectedValue, int newValue) {// 讀取目前的value值int oldValue = value;// 比較目前value值是否等於期望值if (oldValue == expectedValue)value = newValue; // 如果相等,則更新value的值return oldValue; // 返回寫入之前的值}public synchronized boolean compareAndSet(int expectedValue, int newValue) {return (expectedValue == compareAndSwap(expectedValue, newValue));}}
當多個線程嘗試使用CAS同時更新一個變量時,只有其中一個線程能夠更新變量的值,而其他線程都將失敗。但是,與獲取鎖實現的線程不一樣,這裡失敗的線程並不會被掛起,而是被告知在這次競爭中失敗,並可以再次嘗試(或者選擇不嘗試,做一些其他恢復操作等)。這種方式的靈活性大大減少與鎖相關的活躍性風險。
CAS的典型使用模式:首先從V中讀取值A,並根據A計算新值B,然後在通過CAS以原子方式將V中的值由A變為B,前提是這個期間沒有其他線程將V的值修改為其他值。由於CAS能檢測到來自其他線程的幹擾,因此即使不用鎖也能夠實現原子的讀-改-寫操作。
非阻塞的計數器
下面我們使用CAS來實現一個線程安全的計數器。遞增操作採用標準方式:讀取舊值,根據它計算新值,並使用CAS來設置這個新值。如果CAS失敗,那麼該操作將立即重試。反覆的重試也稱為自旋。通常,反覆地重試是一種合理的策略,但是在一些競爭激烈的情況下,最好的方式實在重試之前先等待一段時間或者回退,從而避免造成活鎖問題。
下面基於「CAS+自旋」實現計數器:
public class CasCounter {private SimulatedCAS value;public int getValue() {return value.get();}public int increment() {int v;do {v = value.get(); // 獲取舊值} while (v != value.compareAndSwap(v, v + 1)); // 自旋嘗試return v + 1;}}
CasCounter不會阻塞,但是如果其他線程同時更新計數器,那麼會多次執行重試操作。
這樣看來,似乎基於CAS的計數器比基於鎖的計數器性能要差一點,因為它需要執行更多的操作和更複雜的控制流。然而實際情況卻並非如此。雖然Java語言的鎖定語法簡潔,但是JVM和操作在管理鎖時需要完成的工作卻並不簡單,即使用鎖簡單,鎖背後要做的工作卻複雜。
在實現鎖定時需要遍歷JVM中一條非常複雜的代碼路徑,並可能導致作業系統級的鎖定、線程掛起以及上下文切換等操作。並且,在最好情況下(獲取無競爭的鎖),在鎖定時至少需要一次CAS,因此雖然在使用鎖時沒有用到CAS,但實際上也無法節約任何執行開銷,也就是說會比CAS執行更多的操作。
在程序內部執行CAS時是不需要執行JVM代碼、系統調用或者線程調度操作。在應用級別看起來越長的代碼路徑,如果加上JVM和作業系統中的代碼調用,那麼事實上卻變得更短。
而CAS的主要缺點在於:它將使調用者來處理競爭問題(通過重試、回退和放棄),而在鎖中能自動處理競爭問題(線程在獲取鎖之間將一直被阻塞)。
CAS的性能也會隨著處理器數量的不同、體系架構的不同甚至處理器版本的不同而產生變化。
ABA問題
ABA問題是指:如果V的值首先由A變成了B,再由B變成了A,雖然V中的值A好像沒有變,但是在某些算法中,A的屬性卻是變了。對於保護的變量是數值類型是不需要關心ABA問題,但是如果是對象,就需要注意。
一個相對簡單的解決辦法是:不是更新某個引用的值,而是更新兩個值,包括一個引用和一個版本號。即使這個值由A變成了B,然後又變成了A,版本號也將是不同的。
AtomicStampedReference和AtomicMarkableReference支持在兩個變量上執行原子的條件更新。
AtomicStampedReference將更新一個「對象-引用」二元組,通過在引用上加上「版本號」,從而避免了ABA問題。
同樣,AtomicMarkableReference將更新一個「對象引用-布爾值」二元組。
JVM對CAS的支持
在Java 5.0之前,如果不編寫明確的代碼,那麼就無法執行CAS。在Java 5.0中引入了底層的支持,在int、long和對象的引用等類型上都公開了CAS操作,並且JVM把它們編譯為底層硬體提供的最有效方法。在支持CAS的平臺上,運行時把它們編譯為相應的(多條)機器指令。在最壞情況下,如果不支持CAS指令,那麼JVM將使用自旋鎖。
在原子變量類(例如java.util.concurrent.atomic中的AtomicXxx)中使用了這些底層的JVM支持為數字類型和引用類型提供的一種高效的CAS操作,而在java.util.concurrent中的大多數類在實現時則直接或間接地使用了這些原子變量類。
原子類概覽
Java JDK並發包中提供的原子類很豐富,可以分為五個類別:標量類(基本數據類)、對象引用類、數組類、對象屬性更新器類和累加器類。
Java提供的原子類裡面CAS一般被實現為compareAndSet(),compareAndSet()的語義和CAS指令語義的差別僅僅是返回值的不同,compareAndSet()裡面如果更新成功,則會返回true,否則返回false。
do {// 獲取當前值oldV = xxxx;// 根據當前值計算新值newV = ...oldV...}while(!compareAndSet(oldV,newV);
原子標量類(原子基本數據類)
相關實現類有AtomicBoolean、AtomicInteger和AtomicLong,提供的方法主如下:
getAndIncrement() // 原子化 i++getAndDecrement() // 原子化的 i--incrementAndGet() // 原子化的 ++idecrementAndGet() // 原子化的 --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問題。
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()方法。
在實際情況中,如果僅需一個計數器或者序列生成器,那麼可以直接使AtomicInteger或者AtomicLong,它們能提供原子的遞增方法以及其他算術方法。
小結
原子變量比鎖的粒度更細,量級更輕。原子變量類也相當於一種泛化的volatile變量。原子變量較於鎖來說,在性能和降低活躍性方面都表現很好,但是原子變量是將發生競爭的範圍縮小到單個變量上,當需要解決多個共享變量的原子性問題,還是建議使用鎖。