Unsafe與CAS(ABA)

小結: 1:unsafe是jvm用於操作底層系統的方法類,延申下:前些天學習hbase,把ons的堆積消息消費,剛開始沒注意堆棧信息,用公網消費 tps能5k左右,一段時間後發現降到30~50,再也上不去(以爲是自己線程存在同步導致下降,驗證:註釋所有可疑無果,後面放到華東2內網消費就回到1w上下),不過堆棧信息一致提示如下,最終確認環境公網限制的。

TID: 150 STATE: WAITING
sun.misc.Unsafe.park(Native Method)
java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)

stackoverflow有篇此(線程池同步)問題: https://stackoverflow.com/questions/24241335/waiting-at-sun-misc-unsafe-parknative-method?r=SearchResults 

2:CAS:Compare and Swap即比較並交換,三個value進行標識 1內存val,2預期內存val ,3修改val進行一致性維護,不過還是存在ABA問題漏洞


Unsafe

簡單講一下這個類。Java無法直接訪問底層操作系統,而是通過本地(native)方法來訪問。不過儘管如此,JVM還是開了一個後門,JDK中有一個類Unsafe,它提供了硬件級別的原子操作

這個類儘管裏面的方法都是public的,但是並沒有辦法使用它們,JDK API文檔也沒有提供任何關於這個類的方法的解釋。總而言之,對於Unsafe類的使用都是受限制的,只有授信的代碼才能獲得該類的實例,當然JDK庫裏面的類是可以隨意使用的。

從第一行的描述可以瞭解到Unsafe提供了硬件級別的操作,比如說獲取某個屬性在內存中的位置,比如說修改對象的字段值,即使它是私有的。不過Java本身就是爲了屏蔽底層的差異,對於一般的開發而言也很少會有這樣的需求。

舉兩個例子,比方說:

public native long staticFieldOffset(Field paramField);

這個方法可以用來獲取給定的paramField的內存地址偏移量,這個值對於給定的field是唯一的且是固定不變的。再比如說:

public native int arrayBaseOffset(Class paramClass);
public native int arrayIndexScale(Class paramClass);

前一個方法是用來獲取數組第一個元素的偏移地址,後一個方法是用來獲取數組的轉換因子即數組中元素的增量地址的。最後看三個方法:

public native long allocateMemory(long paramLong);
public native long reallocateMemory(long paramLong1, long paramLong2);
public native void freeMemory(long paramLong);

分別用來分配內存,擴充內存和釋放內存的。

當然這需要有一定的C/C++基礎,對內存分配有一定的瞭解,這也是爲什麼我一直認爲C/C++開發者轉行做Java會有優勢的原因。

 

CAS

CAS,Compare and Swap即比較並交換,設計併發算法時常用到的一種技術,java.util.concurrent包全完建立在CAS之上,沒有CAS也就沒有此包,可見CAS的重要性。

當前的處理器基本都支持CAS,只不過不同的廠家的實現不一樣罷了。CAS有三個操作數:內存值V、舊的預期值A、要修改的值B,當且僅當預期值A和內存值V相同時,將內存值修改爲B並返回true,否則什麼都不做並返回false

CAS也是通過Unsafe實現的,看下Unsafe下的三個方法:

public final native boolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3);

public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);

public final native boolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3);

就拿中間這個比較並交換Int值爲例好了,如果我們不用CAS,那麼代碼大致是這樣的:

 

 1 public int i = 1;
 2     
 3 public boolean compareAndSwapInt(int j)
 4 {
 5     if (i == 1)
 6     {
 7         i = j;
 8         return true;
 9     }
10     return false;
11 }

 

當然這段代碼在併發下是肯定有問題的,有可能線程1運行到了第5行正準備運行第7行,線程2運行了,把i修改爲10,線程切換回去,線程1由於先前已經滿足第5行的if了,所以導致兩個線程同時修改了變量i。

解決辦法也很簡單,給compareAndSwapInt方法加鎖同步就行了,這樣,compareAndSwapInt方法就變成了一個原子操作。CAS也是一樣的道理,比較、交換也是一組原子操作,不會被外部打斷,先根據paramLong/paramLong1獲取到內存當中當前的內存值V,在將內存值V和原值A作比較,要是相等就修改爲要修改的值B,由於CAS都是硬件級別的操作,因此效率會高一些。

 

由CAS分析AtomicInteger原理

java.util.concurrent.atomic包下的原子操作類都是基於CAS實現的,下面拿AtomicInteger分析一下,首先是AtomicInteger類變量的定義:

 

 1 private static final Unsafe unsafe = Unsafe.getUnsafe();
 2 private static final long valueOffset;
 3 
 4 static {
 5  try {
 6     valueOffset = unsafe.objectFieldOffset
 7         (AtomicInteger.class.getDeclaredField("value"));
 8   } catch (Exception ex) { throw new Error(ex); }
 9 }
10 
11 private volatile int value;

關於這段代碼中出現的幾個成員屬性:

1、Unsafe是CAS的核心類,前面已經講過了

2、valueOffset表示的是變量值在內存中的偏移地址,因爲Unsafe就是根據內存偏移地址獲取數據的原值的

3、value是用volatile修飾的,這是非常關鍵的

下面找一個方法getAndIncrement來研究一下AtomicInteger是如何實現的,比如我們常用的addAndGet方法: 1 public final int addAndGet(int delta) { 2 for (;;) { 3 int current = get(); 4 int next = current + delta; 5 if (compareAndSet(current, next)) 6 return next; 7 } 8 }

 

 

 1 public final int get() {
 2     return value;
 3 }

這段代碼如何在不加鎖的情況下通過CAS實現線程安全,我們不妨考慮一下方法的執行:

1、AtomicInteger裏面的value原始值爲3,即主內存中AtomicInteger的value爲3,根據Java內存模型,線程1和線程2各自持有一份value的副本,值爲3

2、線程1運行到第三行獲取到當前的value爲3,線程切換

3、線程2開始運行,獲取到value爲3,利用CAS對比內存中的值也爲3,比較成功,修改內存,此時內存中的value改變比方說是4,線程切換

4、線程1恢復運行,利用CAS比較發現自己的value爲3,內存中的value爲4,得到一個重要的結論-->此時value正在被另外一個線程修改,所以我不能去修改它

5、線程1的compareAndSet失敗,循環判斷,因爲value是volatile修飾的,所以它具備可見性的特性,線程2對於value的改變能被線程1看到,只要線程1發現當前獲取的value是4,內存中的value也是4,說明線程2對於value的修改已經完畢並且線程1可以嘗試去修改它

6、最後說一點,比如說此時線程3也準備修改value了,沒關係,因爲比較-交換是一個原子操作不可被打斷,線程3修改了value,線程1進行compareAndSet的時候必然返回的false,這樣線程1會繼續循環去獲取最新的value並進行compareAndSet,直至獲取的value和內存中的value一致爲止

整個過程中,利用CAS機制保證了對於value的修改的線程安全性。

 

CAS的缺點

CAS看起來很美,但這種操作顯然無法涵蓋併發下的所有場景,並且CAS從語義上來說也不是完美的,存在這樣一個邏輯漏洞:如果一個變量V初次讀取的時候是A值,並且在準備賦值的時候檢查到它仍然是A值,那我們就能說明它的值沒有被其他線程修改過了嗎?如果在這段期間它的值曾經被改成了B,然後又改回A,那CAS操作就會誤認爲它從來沒有被修改過。這個漏洞稱爲CAS操作的"ABA"問題。java.util.concurrent包爲了解決這個問題,提供了一個帶有標記的原子引用類"AtomicStampedReference",它可以通過控制變量值的版本來保證CAS的正確性。不過目前來說這個類比較"雞肋",大部分情況下ABA問題並不會影響程序併發的正確性,如果需要解決ABA問題,使用傳統的互斥同步可能迴避原子類更加高效。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章