Java 中的 Unsafe 和 CAS,你知道嗎?
來(lái)源 |?https://urlify.cn/nAVjM3
Unsafe
簡(jiǎn)單講一下這個(gè)類。Java無(wú)法直接訪問(wèn)底層操作系統(tǒng),而是通過(guò)本地(native)方法來(lái)訪問(wèn)。不過(guò)盡管如此,JVM還是開(kāi)了一個(gè)后門,JDK中有一個(gè)類Unsafe,它提供了硬件級(jí)別的原子操作。
這個(gè)類盡管里面的方法都是public的,但是并沒(méi)有辦法使用它們,JDK API文檔也沒(méi)有提供任何關(guān)于這個(gè)類的方法的解釋??偠灾?,對(duì)于Unsafe類的使用都是受限制的,只有授信的代碼才能獲得該類的實(shí)例,當(dāng)然JDK庫(kù)里面的類是可以隨意使用的。
從第一行的描述可以了解到Unsafe提供了硬件級(jí)別的操作,比如說(shuō)獲取某個(gè)屬性在內(nèi)存中的位置,比如說(shuō)修改對(duì)象的字段值,即使它是私有的。不過(guò)Java本身就是為了屏蔽底層的差異,對(duì)于一般的開(kāi)發(fā)而言也很少會(huì)有這樣的需求。
舉兩個(gè)例子,比方說(shuō):
public native long staticFieldOffset(Field paramField);
這個(gè)方法可以用來(lái)獲取給定的paramField的內(nèi)存地址偏移量,這個(gè)值對(duì)于給定的field是唯一的且是固定不變的。再比如說(shuō):
public native int arrayBaseOffset(Class paramClass);
public native int arrayIndexScale(Class paramClass);
前一個(gè)方法是用來(lái)獲取數(shù)組第一個(gè)元素的偏移地址,后一個(gè)方法是用來(lái)獲取數(shù)組的轉(zhuǎn)換因子即數(shù)組中元素的增量地址的。最后看三個(gè)方法:
public native long allocateMemory(long paramLong);
public native long reallocateMemory(long paramLong1, long paramLong2);
public native void freeMemory(long paramLong);
分別用來(lái)分配內(nèi)存,擴(kuò)充內(nèi)存和釋放內(nèi)存的。
當(dāng)然這需要有一定的C/C++基礎(chǔ),對(duì)內(nèi)存分配有一定的了解,這也是為什么我一直認(rèn)為C/C++開(kāi)發(fā)者轉(zhuǎn)行做Java會(huì)有優(yōu)勢(shì)的原因。
CAS
CAS,Compare and Swap即比較并交換,設(shè)計(jì)并發(fā)算法時(shí)常用到的一種技術(shù),java.util.concurrent包全完建立在CAS之上,沒(méi)有CAS也就沒(méi)有此包,可見(jiàn)CAS的重要性。
當(dāng)前的處理器基本都支持CAS,只不過(guò)不同的廠家的實(shí)現(xiàn)不一樣罷了。CAS有三個(gè)操作數(shù):內(nèi)存值V、舊的預(yù)期值A(chǔ)、要修改的值B,當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí),將內(nèi)存值修改為B并返回true,否則什么都不做并返回false。
CAS也是通過(guò)Unsafe實(shí)現(xiàn)的,看下Unsafe下的三個(gè)方法:
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);
就拿中間這個(gè)比較并交換Int值為例好了,如果我們不用CAS,那么代碼大致是這樣的:
public int i = 1;
public boolean compareAndSwapInt(int j) {
if (i == 1) {
i = j;
return true;
}
return false;
}
當(dāng)然這段代碼在并發(fā)下是肯定有問(wèn)題的,有可能線程1運(yùn)行到了第5行正準(zhǔn)備運(yùn)行第7行,線程2運(yùn)行了,把i修改為10,線程切換回去,線程1由于先前已經(jīng)滿足第5行的if了,所以導(dǎo)致兩個(gè)線程同時(shí)修改了變量i。
解決辦法也很簡(jiǎn)單,給compareAndSwapInt方法加鎖同步就行了,這樣,compareAndSwapInt方法就變成了一個(gè)原子操作。CAS也是一樣的道理,比較、交換也是一組原子操作,不會(huì)被外部打斷,先根據(jù)paramLong/paramLong1獲取到內(nèi)存當(dāng)中當(dāng)前的內(nèi)存值V,在將內(nèi)存值V和原值A(chǔ)作比較,要是相等就修改為要修改的值B,由于CAS都是硬件級(jí)別的操作,因此效率會(huì)高一些。
由CAS分析AtomicInteger原理
java.util.concurrent.atomic包下的原子操作類都是基于CAS實(shí)現(xiàn)的,下面拿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;
關(guān)于這段代碼中出現(xiàn)的幾個(gè)成員屬性:
1、Unsafe是CAS的核心類,前面已經(jīng)講過(guò)了
2、valueOffset表示的是變量值在內(nèi)存中的偏移地址,因?yàn)閁nsafe就是根據(jù)內(nèi)存偏移地址獲取數(shù)據(jù)的原值的
3、value是用volatile修飾的,這是非常關(guān)鍵的
下面找一個(gè)方法getAndIncrement來(lái)研究一下AtomicInteger是如何實(shí)現(xiàn)的,比如我們常用的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;
}
這段代碼如何在不加鎖的情況下通過(guò)CAS實(shí)現(xiàn)線程安全,我們不妨考慮一下方法的執(zhí)行:
1、AtomicInteger里面的value原始值為3,即主內(nèi)存中AtomicInteger的value為3,根據(jù)Java內(nèi)存模型,線程1和線程2各自持有一份value的副本,值為3
2、線程1運(yùn)行到第三行獲取到當(dāng)前的value為3,線程切換
3、線程2開(kāi)始運(yùn)行,獲取到value為3,利用CAS對(duì)比內(nèi)存中的值也為3,比較成功,修改內(nèi)存,此時(shí)內(nèi)存中的value改變比方說(shuō)是4,線程切換
4、線程1恢復(fù)運(yùn)行,利用CAS比較發(fā)現(xiàn)自己的value為3,內(nèi)存中的value為4,得到一個(gè)重要的結(jié)論–>此時(shí)value正在被另外一個(gè)線程修改,所以我不能去修改它
5、線程1的compareAndSet失敗,循環(huán)判斷,因?yàn)関alue是volatile修飾的,所以它具備可見(jiàn)性的特性,線程2對(duì)于value的改變能被線程1看到,只要線程1發(fā)現(xiàn)當(dāng)前獲取的value是4,內(nèi)存中的value也是4,說(shuō)明線程2對(duì)于value的修改已經(jīng)完畢并且線程1可以嘗試去修改它
6、最后說(shuō)一點(diǎn),比如說(shuō)此時(shí)線程3也準(zhǔn)備修改value了,沒(méi)關(guān)系,因?yàn)楸容^-交換是一個(gè)原子操作不可被打斷,線程3修改了value,線程1進(jìn)行compareAndSet的時(shí)候必然返回的false,這樣線程1會(huì)繼續(xù)循環(huán)去獲取最新的value并進(jìn)行compareAndSet,直至獲取的value和內(nèi)存中的value一致為止
整個(gè)過(guò)程中,利用CAS機(jī)制保證了對(duì)于value的修改的線程安全性。
CAS的缺點(diǎn)
CAS看起來(lái)很美,但這種操作顯然無(wú)法涵蓋并發(fā)下的所有場(chǎng)景,并且CAS從語(yǔ)義上來(lái)說(shuō)也不是完美的,存在這樣一個(gè)邏輯漏洞:如果一個(gè)變量V初次讀取的時(shí)候是A值,并且在準(zhǔn)備賦值的時(shí)候檢查到它仍然是A值,那我們就能說(shuō)明它的值沒(méi)有被其他線程修改過(guò)了嗎?如果在這段期間它的值曾經(jīng)被改成了B,然后又改回A,那CAS操作就會(huì)誤認(rèn)為它從來(lái)沒(méi)有被修改過(guò)。這個(gè)漏洞稱為CAS操作的”ABA”問(wèn)題。java.util.concurrent包為了解決這個(gè)問(wèn)題,提供了一個(gè)帶有標(biāo)記的原子引用類”AtomicStampedReference”,它可以通過(guò)控制變量值的版本來(lái)保證CAS的正確性。不過(guò)目前來(lái)說(shuō)這個(gè)類比較”雞肋”,大部分情況下ABA問(wèn)題并不會(huì)影響程序并發(fā)的正確性,如果需要解決ABA問(wèn)題,使用傳統(tǒng)的互斥同步可能會(huì)比原子類更加高效。
-?END?-
往期推薦
下方二維碼關(guān)注我

互聯(lián)網(wǎng)草根,堅(jiān)持分享技術(shù)、創(chuàng)業(yè)、產(chǎn)品等心得和總結(jié)~

點(diǎn)擊“閱讀原文”,領(lǐng)取 2020 年最新免費(fèi)技術(shù)資料大全
