摘要:并發(fā)包將這種無(wú)鎖方案封裝提煉之后,實(shí)現(xiàn)了一系列的原子類(lèi)。無(wú)鎖方案相對(duì)互斥鎖方案,最大的好處就是性能。作為一條指令,指令本身是能夠保證原子性的。
前面我們多次提到一個(gè)累加器的例子,示例代碼如下。在這個(gè)例子中,add10K() 這個(gè)方法不是線(xiàn)程安全的,問(wèn)題就出在變量 count 的可見(jiàn)性和 count+=1 的原子性上??梢?jiàn)性問(wèn)題可以用 volatile 來(lái)解決,而原子性問(wèn)題我們前面一直都是采用的互斥鎖方案。
public class Test { long count = 0; void add10K() { int idx = 0; while(idx++ < 10000) { count += 1; } } }
其實(shí)對(duì)于簡(jiǎn)單的原子性問(wèn)題,還有一種無(wú)鎖方案。Java SDK 并發(fā)包將這種無(wú)鎖方案封裝提煉之后,實(shí)現(xiàn)了一系列的原子類(lèi)。
在下面的代碼中,我們將原來(lái)的 long 型變量 count 替換為了原子類(lèi) AtomicLong,原來(lái)的count +=1 替換成了 count.getAndIncrement(),僅需要這兩處簡(jiǎn)單的改動(dòng)就能使 add10K() 方法變成線(xiàn)程安全的,原子類(lèi)的使用還是挺簡(jiǎn)單的。
public class Test { AtomicLong count = new AtomicLong(0); void add10K() { int idx = 0; while(idx++ < 10000) { count.getAndIncrement(); } } }
無(wú)鎖方案相對(duì)互斥鎖方案,最大的好處就是性能?;コ怄i方案為了保證互斥性,需要執(zhí)行加鎖、解鎖操作,而加鎖、解鎖操作本身就消耗性能;同時(shí)拿不到鎖的線(xiàn)程還會(huì)進(jìn)入阻塞狀態(tài),進(jìn)而觸發(fā)線(xiàn)程切換,線(xiàn)程切換對(duì)性能的消耗也很大。 相比之下,無(wú)鎖方案則完全沒(méi)有加鎖、解鎖的性能消耗,同時(shí)還能保證互斥性,既解決了問(wèn)題,又沒(méi)有帶來(lái)新的問(wèn)題,可謂絕佳方案。那它是如何做到的呢?
無(wú)鎖方案的實(shí)現(xiàn)原理其實(shí)原子類(lèi)性能高的秘密很簡(jiǎn)單,硬件支持而已。CPU 為了解決并發(fā)問(wèn)題,提供了 CAS 指令(CAS,全稱(chēng)是 Compare And Swap,即“比較并交換”)。CAS 指令包含 3 個(gè)參數(shù):共享變量的內(nèi)存地址 A、用于比較的值 B 和共享變量的新值 C;并且只有當(dāng)內(nèi)存中地址 A 處的值等于 B 時(shí),才能將內(nèi)存中地址 A 處的值更新為新值 C。作為一條 CPU 指令,CAS 指令本身是能夠保證原子性的。
你可以通過(guò)下面 CAS 指令的模擬代碼來(lái)理解 CAS 的工作原理。在下面的模擬程序中有兩個(gè)參數(shù),一個(gè)是期望值 expect,另一個(gè)是需要寫(xiě)入的新值 newValue, 只有當(dāng)目前 count 的值和期望值 expect 相等時(shí),才會(huì)將 count 更新為 newValue
class SimulatedCAS{ int count; synchronized int cas( int expect, int newValue){ // 讀目前 count 的值 int curValue = count; // 比較目前 count 值是否 == 期望值 if(curValue == expect){ // 如果是,則更新 count 的值 count = newValue; } // 返回寫(xiě)入前的值 return curValue; } }
count += 1的一個(gè)核心問(wèn)題是:基于內(nèi)存中 count 的當(dāng)前值 A 計(jì)算出來(lái)的 count+=1 為 A+1,在將 A+1 寫(xiě)入內(nèi)存的時(shí)候,很可能此時(shí)內(nèi)存中 count 已經(jīng)被其他線(xiàn)程更新過(guò)了,這樣就會(huì)導(dǎo)致錯(cuò)誤地覆蓋其他線(xiàn)程寫(xiě)入的值。
使用 CAS 來(lái)解決并發(fā)問(wèn)題,一般都會(huì)伴隨著自旋,而所謂自旋,其實(shí)就是循環(huán)嘗試。例如,實(shí)現(xiàn)一個(gè)線(xiàn)程安全的count += 1操作,CAS+ 自旋的實(shí)現(xiàn)方案如下所示,首先計(jì)算 newValue = count+1,如果 cas(count,newValue) 返回的值不等于 count,則意味著線(xiàn)程在執(zhí)行完代碼①處之后,執(zhí)行代碼②處之前,count 的值被其他線(xiàn)程更新過(guò)。那此時(shí)該怎么處理呢?可以采用自旋方案,就像下面代碼中展示的,可以重新讀 count 最新的值來(lái)計(jì)算 newValue 并嘗試再次更新,直到成功。
class SimulatedCAS{ volatile int count; // 實(shí)現(xiàn) count+=1 addOne(){ do { newValue = count+1; //① }while(count != cas(count,newValue) //② } // 模擬實(shí)現(xiàn) CAS,僅用來(lái)幫助理解 synchronized int cas( int expect, int newValue){ // 讀目前 count 的值 int curValue = count; // 比較目前 count 值是否 == 期望值 if(curValue == expect){ // 如果是,則更新 count 的值 count= newValue; } // 返回寫(xiě)入前的值 return curValue; } }
通過(guò)上面的示例代碼,想必你已經(jīng)發(fā)現(xiàn)了,CAS 這種無(wú)鎖方案,完全沒(méi)有加鎖、解鎖操作,即便兩個(gè)線(xiàn)程完全同時(shí)執(zhí)行 addOne() 方法,也不會(huì)有線(xiàn)程被阻塞,所以相對(duì)于互斥鎖方案來(lái)說(shuō),性能好了很多。
但是在 CAS 方案中,有一個(gè)問(wèn)題可能會(huì)常被你忽略,那就是ABA問(wèn)題。
前面我們提到“如果 cas(count,newValue) 返回的值不等于count,意味著線(xiàn)程在執(zhí)行完代碼①處之后,執(zhí)行代碼②處之前,count 的值被其他線(xiàn)程更新過(guò)。那如果 cas(count,newValue) 返回的值等于count,是否就能夠認(rèn)為 count 的值沒(méi)有被其他線(xiàn)程更新過(guò)呢?
顯然不是的,假設(shè) count 原本是 A,線(xiàn)程 T1 在執(zhí)行完代碼①處之后,執(zhí)行代碼②處之前,有可能 count 被線(xiàn)程 T2 更新成了 B,之后又被 T3 更新回了 A,這樣線(xiàn)程 T1 雖然看到的一直是 A,但是其實(shí)已經(jīng)被其他線(xiàn)程更新過(guò)了,這就是 ABA 問(wèn)題。
可能大多數(shù)情況下我們并不關(guān)心 ABA 問(wèn)題,例如數(shù)值的原子遞增,但也不能所有情況下都不關(guān)心,例如原子化的更新對(duì)象很可能就需要關(guān)心 ABA 問(wèn)題,因?yàn)閮蓚€(gè) A 雖然相等,但是第二個(gè) A 的屬性可能已經(jīng)發(fā)生變化了。所以在使用 CAS 方案的時(shí)候,一定要先 check 一下。
Java 如何實(shí)現(xiàn)原子化的 count += 1在本文開(kāi)始部分,我們使用原子類(lèi) AtomicLong 的 getAndIncrement() 方法替代了count+1
1,從而實(shí)現(xiàn)了線(xiàn)程安全。原子類(lèi) AtomicLong 的 getAndIncrement() 方法內(nèi)部就是基于 CAS 實(shí)現(xiàn)的,下面我們來(lái)看看 Java 是如何使用 CAS 來(lái)實(shí)現(xiàn)原子化的
在 Java 1.8 版本中,getAndIncrement() 方法會(huì)轉(zhuǎn)調(diào) unsafe.getAndAddLong() 方法。這里 this 和 valueOffset 兩個(gè)參數(shù)可以唯一確定共享變量的內(nèi)存地址。
final long getAndIncrement() { return unsafe.getAndAddLong( this, valueOffset, 1L); }
unsafe.getAndAddLong() 方法的源碼如下,該方法首先會(huì)在內(nèi)存中讀取共享變量的值,之后循環(huán)調(diào)用 compareAndSwapLong() 方法來(lái)嘗試設(shè)置共享變量的值,直到成功為止。compareAndSwapLong() 是一個(gè) native 方法,只有當(dāng)內(nèi)存中共享變量的值等于 expected 時(shí),才會(huì)將共享變量的值更新為 x,并且返回 true;否則返回 fasle。compareAndSwapLong 的語(yǔ)義和 CAS 指令的語(yǔ)義的差別僅僅是返回值不同而已。
public final long getAndAddLong( Object o, long offset, long delta){ long v; do { // 讀取內(nèi)存中的值 v = getLongVolatile(o, offset); } while (!compareAndSwapLong( o, offset, v, v + delta)); return v; } // 原子性地將變量更新為 x // 條件是內(nèi)存中的值等于 expected // 更新成功則返回 true native boolean compareAndSwapLong( Object o, long offset, long expected, long x);
另外,需要你注意的是,getAndAddLong() 方法的實(shí)現(xiàn),基本上就是 CAS 使用的經(jīng)典范例。所以請(qǐng)你再次體會(huì)下面這段抽象后的代碼片段,它在很多無(wú)鎖程序中經(jīng)常出現(xiàn)。Java 提供的原子類(lèi)里面 CAS 一般被實(shí)現(xiàn)為 compareAndSet(),compareAndSet() 的語(yǔ)義和 CAS 指令的語(yǔ)義的差別僅僅是返回值不同而已,compareAndSet() 里面如果更新成功,則會(huì)返回 true,否則返回 false。
do { // 獲取當(dāng)前值 oldV = xxxx; // 根據(jù)當(dāng)前值計(jì)算新值 newV = ...oldV... }while(!compareAndSet(oldV,newV);原子類(lèi)概覽
Java SDK 并發(fā)包里提供的原子類(lèi)內(nèi)容很豐富,我們可以將它們分為五個(gè)類(lèi)別:
原子化的基本數(shù)據(jù)類(lèi)型
原子化的對(duì)象引用類(lèi)型
原子化數(shù)組
原子化對(duì)象屬性更新器
原子化的累加器
。這五個(gè)類(lèi)別提供的方法基本上是相似的,并且每個(gè)類(lèi)別都有若干原子類(lèi),你可以通過(guò)下面的原子類(lèi)組成概覽圖來(lái)獲得一個(gè)全局的印象。下面我們?cè)敿?xì)解讀這五個(gè)類(lèi)別。
1. 原子化的基本數(shù)據(jù)類(lèi)型相關(guān)實(shí)現(xiàn)有 AtomicBoolean、AtomicInteger 和 AtomicLong,提供的方法主要有以下這些,詳情你可以參考 SDK 的源代碼,都很簡(jiǎn)單,這里就不詳細(xì)介紹了。
getAndIncrement() // 原子化 i++ getAndDecrement() // 原子化的 i-- incrementAndGet() // 原子化的 ++i decrementAndGet() // 原子化的 --i // 當(dāng)前值 +=delta,返回 += 前的值 getAndAdd(delta) // 當(dāng)前值 +=delta,返回 += 后的值 addAndGet(delta) //CAS 操作,返回是否成功 compareAndSet(expect, update) // 以下四個(gè)方法 // 新值可以通過(guò)傳入 func 函數(shù)來(lái)計(jì)算 getAndUpdate(func) updateAndGet(func) getAndAccumulate(x,func) accumulateAndGet(x,func)2. 原子化的對(duì)象引用類(lèi)型
相關(guān)實(shí)現(xiàn)有 AtomicReference、AtomicStampedReference 和 AtomicMarkableReference,利用它們可以實(shí)現(xiàn)對(duì)象引用的原子化更新。AtomicReference 提供的方法和原子化的基本數(shù)據(jù)類(lèi)型差不多,這里不再贅述。不過(guò)需要注意的是,對(duì)象引用的更新需要重點(diǎn)關(guān)注 ABA 問(wèn)題,AtomicStampedReference 和 AtomicMarkableReference 這兩個(gè)原子類(lèi)可以解決 ABA 問(wèn)題。
解決 ABA 問(wèn)題的思路其實(shí)很簡(jiǎn)單,增加一個(gè)版本號(hào)維度就可以了。每次執(zhí)行 CAS 操作,附加再更新一個(gè)版本號(hào),只要保證版本號(hào)是遞增的,那么即便 A 變成 B 之后再變回 A,版本號(hào)也不會(huì)變回來(lái)(版本號(hào)遞增的)。AtomicStampedReference 實(shí)現(xiàn)的 CAS 方法就增加了版本號(hào)參數(shù),方法簽名如下:
boolean compareAndSet( V expectedReference, V newReference, int expectedStamp, int newStamp)
AtomicMarkableReference 的實(shí)現(xiàn)機(jī)制則更簡(jiǎn)單,將版本號(hào)簡(jiǎn)化成了一個(gè) Boolean 值,方法簽名如下:
boolean compareAndSet( V expectedReference, V newReference, boolean expectedMark, boolean newMark)3. 原子化數(shù)組
相關(guān)實(shí)現(xiàn)有 AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray,利用這些原子類(lèi),我們可以原子化地更新數(shù)組里面的每一個(gè)元素。這些類(lèi)提供的方法和原子化的基本數(shù)據(jù)類(lèi)型的區(qū)別僅僅是:每個(gè)方法多了一個(gè)數(shù)組的索引參數(shù),所以這里也不再贅述了。
4. 原子化對(duì)象屬性更新器相關(guān)實(shí)現(xiàn)有 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater,利用它們可以原子化地更新對(duì)象的屬性,這三個(gè)方法都是利用反射機(jī)制實(shí)現(xiàn)的,創(chuàng)建更新器的方法如下:
public static AtomicXXXFieldUpdater newUpdater(Class tclass, String fieldName)
需要注意的是,對(duì)象屬性必須是 volatile 類(lèi)型的,只有這樣才能保證可見(jiàn)性;如果對(duì)象屬性不是 volatile 類(lèi)型的,newUpdater() 方法會(huì)拋出 IllegalArgumentException 這個(gè)運(yùn)行時(shí)異常。
你會(huì)發(fā)現(xiàn) newUpdater() 的方法參數(shù)只有類(lèi)的信息,沒(méi)有對(duì)象的引用,而更新對(duì)象的屬性,一定需要對(duì)象的引用,那這個(gè)參數(shù)是在哪里傳入的呢?是在原子操作的方法參數(shù)中傳入的。例如 compareAndSet() 這個(gè)原子操作,相比原子化的基本數(shù)據(jù)類(lèi)型多了一個(gè)對(duì)象引用 obj。原子化對(duì)象屬性更新器相關(guān)的方法,相比原子化的基本數(shù)據(jù)類(lèi)型僅僅是多了對(duì)象引用參數(shù),所以這里也不再贅述了。
boolean compareAndSet( T obj, int expect, int update)5. 原子化的累加器
DoubleAccumulator、DoubleAdder、LongAccumulator 和 LongAdder,這四個(gè)類(lèi)僅僅用來(lái)執(zhí)行累加操作,相比原子化的基本數(shù)據(jù)類(lèi)型,速度更快,但是不支持 compareAndSet() 方法。如果你僅僅需要累加操作,使用原子化的累加器性能會(huì)更好。
總結(jié)無(wú)鎖方案相對(duì)于互斥鎖方案,優(yōu)點(diǎn)非常多,首先性能好,其次是基本不會(huì)出現(xiàn)死鎖問(wèn)題(但可能出現(xiàn)饑餓和活鎖問(wèn)題,因?yàn)樽孕龝?huì)反復(fù)重試)。Java 提供的原子類(lèi)大部分都實(shí)現(xiàn)了 compareAndSet() 方法。
Java 提供的原子類(lèi)能夠解決一些簡(jiǎn)單的原子性問(wèn)題,但你可能會(huì)發(fā)現(xiàn),上面我們所有原子類(lèi)的方法都是針對(duì)一個(gè)共享變量的,如果你需要解決多個(gè)變量的原子性問(wèn)題,建議還是使用互斥鎖方案。原子類(lèi)雖好,但使用要慎之又慎。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/77776.html
摘要:整個(gè)包,按照功能可以大致劃分如下鎖框架原子類(lèi)框架同步器框架集合框架執(zhí)行器框架本系列將按上述順序分析,分析所基于的源碼為。后,根據(jù)一系列常見(jiàn)的多線(xiàn)程設(shè)計(jì)模式,設(shè)計(jì)了并發(fā)包,其中包下提供了一系列基礎(chǔ)的鎖工具,用以對(duì)等進(jìn)行補(bǔ)充增強(qiáng)。 showImg(https://segmentfault.com/img/remote/1460000016012623); 本文首發(fā)于一世流云專(zhuān)欄:https...
摘要:該類(lèi)將整數(shù)值與引用關(guān)聯(lián)起來(lái),可用于原子的更數(shù)據(jù)和數(shù)據(jù)的版本號(hào)。 CAS的全稱(chēng)為Compare And Swap,直譯就是比較交換。是一條CPU的原子指令,其作用是讓CPU先進(jìn)行比較兩個(gè)值是否相等,然后原子地更新某個(gè)位置的值,其實(shí)現(xiàn)方式是基于硬件平臺(tái)的匯編指令,在intel的CPU中,使用的是cmpxchg指令,就是說(shuō)CAS是靠硬件實(shí)現(xiàn)的,從而在硬件層面提升效率。 CSA 原理 利用CP...
摘要:有時(shí)候,由于初期考慮不周,或者后期的需求變化,一些普通變量可能也會(huì)有線(xiàn)程安全的需求。它可以讓你在不改動(dòng)或者極少改動(dòng)原有代碼的基礎(chǔ)上,讓普通的變量也享受操作帶來(lái)的線(xiàn)程安全性,這樣你可以修改極少的代碼,來(lái)獲得線(xiàn)程安全的保證。 有時(shí)候,由于初期考慮不周,或者后期的需求變化,一些普通變量可能也會(huì)有線(xiàn)程安全的需求。如果改動(dòng)不大,我們可以簡(jiǎn)單地修改程序中每一個(gè)使用或者讀取這個(gè)變量的地方。但顯然,這...
摘要:無(wú)鎖算法在并發(fā)編程中,提供了很多并發(fā)編程工具類(lèi)。在包下有一個(gè)包叫,下面所有的工具,我們都稱(chēng)為它是無(wú)鎖算法的一種實(shí)現(xiàn)。相對(duì)于有鎖算法來(lái)說(shuō),無(wú)鎖算法不會(huì)使等待線(xiàn)程休眠或者阻塞。 無(wú)鎖算法 在Java并發(fā)編程中,Java提供了很多并發(fā)編程工具類(lèi)。在JUC包下有一個(gè)包叫atomic,下面所有的工具,我們都稱(chēng)為它是無(wú)鎖算法的一種實(shí)現(xiàn)。 相對(duì)于有鎖算法來(lái)說(shuō),無(wú)鎖算法不會(huì)使等待線(xiàn)程休眠或者阻塞。它的...
摘要:在本例中,講述的無(wú)鎖來(lái)自于并發(fā)包我們將這個(gè)無(wú)鎖的稱(chēng)為。在這里,我們使用二維數(shù)組來(lái)表示的內(nèi)部存儲(chǔ),如下變量存放所有的內(nèi)部元素。為什么使用二維數(shù)組去實(shí)現(xiàn)一個(gè)一維的呢這是為了將來(lái)進(jìn)行動(dòng)態(tài)擴(kuò)展時(shí)可以更加方便。 我們已經(jīng)比較完整得介紹了有關(guān)無(wú)鎖的概念和使用方法。相對(duì)于有鎖的方法,使用無(wú)鎖的方式編程更加考驗(yàn)一個(gè)程序員的耐心和智力。但是,無(wú)鎖帶來(lái)的好處也是顯而易見(jiàn)的,第一,在高并發(fā)的情況下,它比有鎖...
閱讀 2508·2023-04-26 02:47
閱讀 2999·2023-04-26 00:42
閱讀 865·2021-10-12 10:12
閱讀 1372·2021-09-29 09:35
閱讀 1688·2021-09-26 09:55
閱讀 478·2019-08-30 14:00
閱讀 1532·2019-08-29 12:57
閱讀 2350·2019-08-28 18:00