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