java基礎學習總結(十九):Unsafe與CAS
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);
分別用來分配記憶體,擴充記憶體和釋放記憶體的。
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);
引數1:物件所在的類本身的物件(一般這裡是對一個物件的屬性做修改,才會出現併發,所以該物件所存在的類也是有一個物件的)
引數2:這個屬性在這個物件裡面的相對便宜量位置,其實對比時是對比記憶體單元,所以需要屬性的起始位置,而引用就是修改引用地址(根據OS、VM位數和引數配置決定寬度一般是4-8個位元組),int就是修改相關的4個位元組,而long就是修改相關的8個位元組。
獲取偏移量也是通過unsafe的一個方法:objectFieldOffset(Fieldfield)來獲取屬性在物件中的偏移量;靜態變數需要通過:staticFieldOffset(Field field)獲取,呼叫的總方法是:fieldOffset(Fieldfield)
引數3:修改的引用的原始值,用於對比原來的引用和要修改的目標是否一致。
引數4:修改的目標值,要將資料修改成什麼。
就拿中間這個比較並交換Int值為例好了,如果我們不用CAS,那麼程式碼大致是這樣的:
public int i = 1;
public boolean compareAndSwapInt(int j)
{
if (i == 1)
{
i = j;
return true;
}
return false;
}
當然這段程式碼在併發下是肯定有問題的,有可能執行緒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類變數的定義:
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
關於這段程式碼中出現的幾個成員屬性:
1、Unsafe是CAS的核心類,前面已經講過了
2、valueOffset表示的是變數值在記憶體中的偏移地址,因為Unsafe就是根據記憶體偏移地址獲取資料的原值的
3、value是用volatile修飾的,這是非常關鍵的
下面找一個方法getAndIncrement來研究一下AtomicInteger是如何實現的,比如我們常用的addAndGet方法:
public final int addAndGet(int delta) {
for (;;) {
int current = get();
int next = current + delta;
if (compareAndSet(current, next))
return next;
}
}
public final int get() {
return value;
}
這段程式碼如何在不加鎖的情況下通過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問題,使用傳統的互斥同步可能迴避原子類更加高效。