Java并发编程—无锁互斥机制及CAS原理
目錄
一、CAS簡介
二、AtomicInteger代碼演示
三、CAS 實(shí)現(xiàn)
四、弊端
一、CAS簡介
在計算機(jī)科學(xué)中,比較和交換(Conmpare And Swap)是用于實(shí)現(xiàn)多線程同步的原子指令。它將內(nèi)存位置的內(nèi)容與給定值進(jìn)行比較,只有在相同的情況下,將該內(nèi)存位置的內(nèi)容修改為新的給定值。這是作為單個原子操作完成的。 原子性保證新值基于最新信息計算; 如果該值在同一時間被另一個線程更新,則寫入將失敗。操作結(jié)果必須說明是否進(jìn)行替換; 這可以通過一個簡單的布爾響應(yīng)(這個變體通常稱為比較和設(shè)置),或通過返回從內(nèi)存位置讀取的值來完成。
CAS是一種無鎖算法,有3個關(guān)鍵操作數(shù):?內(nèi)存中的原數(shù)據(jù)V,舊的預(yù)期值A(chǔ),需要修改的新值B,當(dāng)內(nèi)存值和舊的內(nèi)存中預(yù)期值相等時,將內(nèi)存中的值更新為新值。
操縱步驟:比較 A 與 V 是否相等。(比較) 如果比較相等,將 B 寫入 V。(交換) 返回操作是否成功。 當(dāng)多個線程同時對某個資源進(jìn)行CAS操作,只能有一個線程操作成功,但是并不會阻塞其他線程,其他線程只會收到操作失敗的信號。可見 CAS 其實(shí)是一個樂觀鎖。
?
如上圖中,主存中保存V值,線程中要使用V值要先從主存中讀取V值到線程的工作內(nèi)存A中,然后計算后變成B值,最后再把B值寫回到內(nèi)存V值中。多個線程共用V值都是如此操作。CAS的核心是在將B值寫入到V之前要比較A值和V值是否相同,如果不相同證明此時V值已經(jīng)被其他線程改變,重新將V值賦給A,并重新計算得到B,如果相同,則將B值賦給V。
如果不使用CAS機(jī)制,看看存在什么問題:假如V=1,現(xiàn)在Thread1要對V進(jìn)行加1,Thread2也要對V進(jìn)行加1,首先Thread1讀取V=1到自己工作內(nèi)存A中此時A=1,假設(shè)Thread2此時也讀取V=1到自己的工作內(nèi)存A中,分別進(jìn)行加1操作后,兩個線程中B的值都為2,此時寫回到V中時發(fā)現(xiàn)V的值為2,但是兩個線程分別對V進(jìn)行加處理結(jié)果卻只加了1有問題。
樂觀鎖與悲觀鎖:CAS屬于樂觀鎖,樂觀鎖就是每次不加鎖而是假設(shè)沒有沖突而去完成某項(xiàng)操作,如果因?yàn)闆_突失敗就重試,直到成功為止。synchronized是悲觀鎖,被一個線程拿到鎖之后,其他線程必須等待該線程釋放鎖,性能較差
二、AtomicInteger代碼演示
在java中,a++不是原子操作,一個簡單的a++操作涉及到三個操作,獲取變量a的內(nèi)存值,將變量a+1,將新值寫入內(nèi)存,這里涉及到了兩次內(nèi)存訪問,如果在多線程環(huán)境下,那么會出現(xiàn)并發(fā)安全問題。AtomicInteger是一個原子操作類,內(nèi)部采用的就是CAS無鎖算法。 這里我們分析一下它的內(nèi)部實(shí)現(xiàn)。
AtomicInteger atomicInteger = new AtomicInteger(0); atomicInteger.getAndSet(1);???? 這里的靜態(tài)代碼塊AtomicInteger對象初始化之前就執(zhí)行,獲取AtomicInteger對象value字段相對AtomicInteger對象的”起始地址”的偏移量,Java對象在內(nèi)存中存儲的布局可以分為三塊區(qū)域:對象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對齊填充(Padding),”起始地址”的偏移量即是對象頭的偏移量。
static {try {valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); } } public final int getAndSet(int newValue) {return unsafe.getAndSetInt(this, valueOffset, newValue); }每次通過內(nèi)存地址(var2)先從內(nèi)存中獲取內(nèi)存中原值(var5),再循環(huán)將內(nèi)存中的原值(var5)與給定內(nèi)存地址(var2)相比較,如果相等則更新指定預(yù)期值(var4),如果不相等則再重試直到成功為止,最后返回舊的內(nèi)存原值var5。
//var1為AtomicInteger對象,var2為內(nèi)存地址值,var4為指定的預(yù)期值 public final int getAndSetInt(Object var1, long var2, int var4) {int var5;do {//unsafe.getIntVolatile調(diào)用本地方法獲取內(nèi)存中值var5 = this.getIntVolatile(var1, var2);} while(!this.compareAndSwapInt(var1, var2, var5, var4));return var5; }三、CAS 實(shí)現(xiàn)
?java.util.concurrent.atomic 包下的原子類 AtomicInteger 中的 compareAndSet 方法最終調(diào)用的是 sum.misc.Unsafe 這個類。 看名稱 Unsafe 就是一個不安全的類,這個類是利用了 Java 的類和包在可見性的的規(guī)則中的一個恰到好處處的漏洞。Unsafe 這個類為了速度,在Java的安全標(biāo)準(zhǔn)上做出了一定的妥協(xié)。再往下尋找我們發(fā)現(xiàn) Unsafe的compareAndSwapInt 是 Native 的方法:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);也就是說,這幾個 CAS 的方法應(yīng)該是使用了本地的方法。所以這幾個方法的具體實(shí)現(xiàn)需要我們自己去 jdk 的源碼中搜索。 最終到搜索 cmpxchg 函數(shù)
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { // 判斷是否是多核 CPUint mp = os::is_MP();__asm { // 將參數(shù)值放入寄存器中mov edx, dest // 注意: dest 是指針類型,這里是把內(nèi)存地址存入 edx 寄存器中mov ecx, exchange_valuemov eax, compare_value // LOCK_IF_MPcmp mp, 0/** 如果 mp = 0,表明是線程運(yùn)行在單核 CPU 環(huán)境下。此時 je 會跳轉(zhuǎn)到 L0 標(biāo)記處,* 也就是越過 _emit 0xF0 指令,直接執(zhí)行 cmpxchg 指令。也就是不在下面的 cmpxchg 指令* 前加 lock 前綴。*/je L0 /** 0xF0 是 lock 前綴的機(jī)器碼,這里沒有使用 lock,而是直接使用了機(jī)器碼的形式。至于這樣做的* 原因可以參考知乎的一個回答:* https://www.zhihu.com/question/50878124/answer/123099923*/ _emit 0xF0L0: /** 比較并交換。簡單解釋一下下面這條指令,熟悉匯編的朋友可以略過下面的解釋:* cmpxchg: 即“比較并交換”指令* dword: 全稱是 double word,在 x86/x64 體系中,一個 * word = 2 byte,dword = 4 byte = 32 bit* ptr: 全稱是 pointer,與前面的 dword 連起來使用,表明訪問的內(nèi)存單元是一個雙字單元* [edx]: [...] 表示一個內(nèi)存單元,edx 是寄存器,dest 指針值存放在 edx 中。* 那么 [edx] 表示內(nèi)存地址為 dest 的內(nèi)存單元* * 這一條指令的意思就是,將 eax 寄存器中的值(compare_value)與 [edx] 雙字內(nèi)存單元中的值* 進(jìn)行對比,如果相同,則將 ecx 寄存器中的值(exchange_value)存入 [edx] 內(nèi)存單元中。*/cmpxchg dword ptr [edx], ecx} }總結(jié)一下 JAVA 的 cas 是怎么實(shí)現(xiàn)的:
- java 的 cas 利用的的是 unsafe 這個類提供的 cas 操作。
- unsafe 的cas 依賴了的是 jvm 針對不同的操作系統(tǒng)實(shí)現(xiàn)的 Atomic::cmpxchg
- Atomic::cmpxchg 的實(shí)現(xiàn)使用了匯編的 cas 操作,并使用 cpu 硬件提供的 lock信號保證其原子性
四、弊端
1. ABA問題
???? CAS在操作的時候會檢查變量的值是否被更改過,如果沒有則更新值,但是帶來一個問題,最開始的值是A,接著變成B,最后又變成了A。經(jīng)過檢查這個值確實(shí)沒有修改過,因?yàn)樽詈蟮闹颠€是A,但是實(shí)際上這個值確實(shí)已經(jīng)被修改過了。為了解決這個問題,在每次進(jìn)行操作的時候加上一個版本號,每次操作的就是兩個值,一個版本號和某個值,A——>B——>A問題就變成了1A——>2B——>3A。在jdk中提供了AtomicStampedReference類解決ABA問題,用Pair這個內(nèi)部類實(shí)現(xiàn),包含兩個屬性,分別代表版本號和引用,在compareAndSet中先對當(dāng)前引用進(jìn)行檢查,再對版本號標(biāo)志進(jìn)行檢查,只有全部相等才更新值。
2. 只能保證一個共享變量的原子操作
???? 多個共享變量操作時,循環(huán)CAS就無法保證操作的原子性,這個時候就可以用鎖。從java1.5開始,JDK提供了AtomicReference類來保證引用對象之間的原子性,就可以把多個變量放在一個對象里來進(jìn)行CAS操作。
3. 循環(huán)時間長CPU開銷較大
???? 在并發(fā)量比較高的情況下,如果許多線程反復(fù)嘗試更新某一個變量,卻又一直更新不成功,循環(huán)往復(fù),會給CPU帶來很大的壓力。
總結(jié)
以上是生活随笔為你收集整理的Java并发编程—无锁互斥机制及CAS原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 分布式实时计算—从霍普金大学数据错误谈谈
- 下一篇: Java并发—锁的四种状态