引言
其實(shí)在我們上一篇文章闡述Java并發(fā)編程中synchronized關(guān)鍵字原理的時候我們曾多次談到過CAS這個概念乙濒,那么它究竟是什么?實(shí)際上我們在之前往往為了解決多線程并行執(zhí)行帶來的線程安全問題去利用加鎖的機(jī)制去將多線程并行執(zhí)行改變?yōu)閱尉€程的串行執(zhí)行卵蛉,而實(shí)則還有另一種手段能夠去避免此類問題的發(fā)生颁股,而這種方案和之前我們所分析的synchronized關(guān)鍵字互斥的原理大相徑庭,它實(shí)則是利用一種自旋的無鎖形式去解決線程安全問題。在之前分析中我們不難發(fā)現(xiàn),當(dāng)一個線程想要執(zhí)行被synchronized修飾的代碼/方法冤馏,為了避免操作共享資源時發(fā)生沖突蚂斤,每次都需要執(zhí)行加鎖策略屁桑,而無鎖則總是假設(shè)對共享資源的訪問沒有沖突,線程可以不停執(zhí)行,無需加鎖,無需等待滤愕,而萬事難遂人愿,一旦發(fā)現(xiàn)沖突怜校,無鎖策略中難道真的不處理嗎间影?并不然,無鎖策略采用一種稱為CAS的技術(shù)來保證線程執(zhí)行的安全性茄茁,這項(xiàng)CAS技術(shù)就是無鎖策略實(shí)現(xiàn)的關(guān)鍵魂贬。
一、無鎖的落地執(zhí)行者 - CAS機(jī)制
我們口中一直所提到的無鎖策略聽起來很完美裙顽,但是當(dāng)真正的需要使用時又該如果落地實(shí)現(xiàn)呢付燥?而CAS機(jī)制則是我們無鎖策略的落地實(shí)現(xiàn)者,CAS全稱Compare And Swap(比較并交換)锦庸,而Java中的CAS實(shí)現(xiàn)最終也是依賴于CPU的原子性指令實(shí)現(xiàn)(我們稍后會分析)机蔗,在CAS機(jī)制中其核心思想如下:
CAS(V,E,N)
- V:需要操作的共享變量
- E:預(yù)期值
- N:新值
如果V值等于E值蒲祈,則將N值賦值給V甘萧。反則如果V值不等于E值萝嘁,則此時說明在當(dāng)前線程寫回之前有其他線程對V做了更改,那么當(dāng)前線程什么都不做扬卷。簡單的來說就是當(dāng)一個線程對V需要做更改時牙言,先在操作之前先保存當(dāng)前時刻共享變量的值,當(dāng)線程操作完成后需要寫回新值時先重新去獲取一下最新的變量值與操作開始之前的保存的預(yù)期值比對怪得,如果相同說明沒有其他線程改過咱枉,那么當(dāng)前線程就執(zhí)行寫入操作。但如果期望值與當(dāng)前線程操作之前保存的不符徒恋,則說明該值已被其他線程修改蚕断,此時不執(zhí)行更新操作,但可以選擇重新讀取該變量再嘗試再次修改該變量入挣,也可以放棄操作亿乳,示意圖如下:
由于CAS操作屬于樂觀派,每次線程操作時都認(rèn)為自己可以成功執(zhí)行径筏,當(dāng)多個線程同時使用CAS操作一個變量時葛假,只有一個會成功執(zhí)行并成功更新,其余均會失敗滋恬,但失敗的線程并不會被掛起聊训,僅是被告知失敗,并且允許再次嘗試恢氯,當(dāng)然也允許失敗的線程放棄操作带斑。基于這樣的原理酿雪,CAS機(jī)制即使沒有鎖遏暴,同樣也能夠得知其他線程對共享資源進(jìn)行了操作并執(zhí)行相應(yīng)的處理措施。同時CAS由于無鎖操作中并沒有鎖的存在指黎,因此不可能出現(xiàn)死鎖的情況朋凉,所以也能得出一個結(jié)論:“CAS天生免疫死鎖”,因?yàn)镃AS本身沒有加鎖醋安。
1.1. 操作系統(tǒng)對于CAS機(jī)制的支持
那么有小伙伴看到這里會疑問杂彭,難道多個線程在同時做CAS操作時不會出現(xiàn)安全問題造成不一致呢?因?yàn)镃AS操作通過我們上面的闡述得知并不是一步到位的吓揪,而是也分為多個步驟來執(zhí)行亲怠,有沒有可能在判斷V和E相同后,正要賦值時柠辞,切換了線程团秽,更改了值?答案非常確定:NO,Why习勤?剛剛在前面我提到過Java中的CAS機(jī)制的最終實(shí)現(xiàn)是依賴于CPU原子性指令實(shí)現(xiàn)踪栋,CAS是一種操作系統(tǒng)原語范疇的指令,是由若干條指令組成的图毕,用于完成某個功能的一個過程夷都,并且原語的執(zhí)行必須是連續(xù)的,在執(zhí)行過程中不允許被中斷予颤,也就是說CAS對于CPU來說是一條的原子指令囤官,不會造成所謂的數(shù)據(jù)不一致問題。
二蛤虐、Java中的魔法指針類 - Unsafe
Unsafe類位于sun.misc包中党饮,中文翻譯過來就是不安全的意思,當(dāng)我們第一次看到這個類時驳庭,我們可能會感到驚訝劫谅,為什么在JDK中會有一個類的名稱被命名為“不安全”,但是你詳細(xì)去研究不難發(fā)現(xiàn)這個類的神奇之處嚷掠,提供的功能十分強(qiáng)大捏检,但是確實(shí)存在些許不安全。Unsafe類存在于sun.misc包中不皆,其內(nèi)部方法操作可以像C的指針一樣直接操作內(nèi)存贯城,當(dāng)我們能夠通過這個類做到和C的指針一樣直接操作內(nèi)存時也就凸顯出此類的不安全性,意味著:
- 不受JVM管理霹娄,也就代表著無法被GC能犯,需要我們手動釋放內(nèi)存,當(dāng)你使用這個類做了一些操作稍有不慎就會出現(xiàn)內(nèi)存泄漏
- Unsafe類中的不少方法中必須提供原始地址(內(nèi)存地址)和被替換對象的地址犬耻,偏移量要自己計(jì)算踩晶,一旦出現(xiàn)問題就是JVM崩潰級別的錯誤,會導(dǎo)致整個Java程序崩潰枕磁,表現(xiàn)為應(yīng)用進(jìn)程直接crash掉
但是我們通過Unsafe類直接操作內(nèi)存渡蜻,也意味著其速度會比普通Java程序更快,在高并發(fā)的條件之下能夠很好地提高效率计济,因此茸苇,從上面幾個角度來看,雖然在一定程度上提升了效率但是也帶來了指針的不安全性沦寂,Unsafe名副其實(shí)学密。所以我們在編寫程序時如果沒有什么特殊要求不應(yīng)該考慮使用它,并且Java官方也不推薦使用(Unsafe也不對外提供構(gòu)造函數(shù))传藏,而且Oracle官方當(dāng)初也打算在Java9中去掉Unsafe類腻暮。但是由于Java并發(fā)包中大量使用了該類彤守,所以O(shè)racle最終在Java并沒有移除掉Unsafe類,只是做了相對于的優(yōu)化與維護(hù)哭靖,而且除開并發(fā)包之外遗增,類似于Netty等框架也在底層中頻繁的使用了該類,所以應(yīng)該慶幸的是保留了該類款青,不然會少去一批優(yōu)秀的開源框架。不過還有值得注意的一點(diǎn)是Unsafe類中的所有方法都是native修飾的霍狰,也就是說Unsafe類中的方法都直接調(diào)用操作系統(tǒng)底層資源執(zhí)行相應(yīng)任務(wù)抡草,關(guān)于Unsafe類的主要功能點(diǎn)如下:
- 類(Class)相關(guān):提供Class和它的靜態(tài)域操縱方法
- 信息(Info)相關(guān):返回某些低級別的內(nèi)存信息
- 數(shù)組(Arrays)相關(guān):提供數(shù)組操縱方法
- 對象(Objects)相關(guān):提供Object和它的域操縱方法
- 內(nèi)存(Memory)相關(guān):提供直接內(nèi)存訪問方法(繞過JVM堆直接操縱本地內(nèi)存)
- 同步(Synchronization)相關(guān):提供低級別同步原語、線程掛起/放下等操縱方法
2.1. 內(nèi)存管理:Unsafe類提供的直接操縱內(nèi)存相關(guān)的方法
//分配內(nèi)存指定大小的內(nèi)存
public native long allocateMemory(long bytes);
//根據(jù)給定的內(nèi)存地址address設(shè)置重新分配指定大小的內(nèi)存
public native long reallocateMemory(long address, long bytes);
//用于釋放allocateMemory和reallocateMemory申請的內(nèi)存
public native void freeMemory(long address);
//將指定對象的給定offset偏移量內(nèi)存塊中的所有字節(jié)設(shè)置為固定值
public native void setMemory(Object o, long offset, long bytes, byte value);
//設(shè)置給定內(nèi)存地址的值
public native void putAddress(long address, long x);
//獲取指定內(nèi)存地址的值
public native long getAddress(long address);
//設(shè)置給定內(nèi)存地址的long值
public native void putLong(long address, long x);
//獲取指定內(nèi)存地址的long值
public native long getLong(long address);
//設(shè)置或獲取指定內(nèi)存的byte值
public native byte getByte(long address);
public native void putByte(long address, byte x);
//其他基本數(shù)據(jù)類型(long,char,float,double,short等)的操作與putByte及getByte相同
.......... 省略代碼
//操作系統(tǒng)的內(nèi)存頁大小
public native int pageSize();
2.2. 對象實(shí)例創(chuàng)建:Unsafe類提供創(chuàng)建對象實(shí)例新的途徑
在之前我們創(chuàng)建類對象實(shí)例時無非通過兩種形式:new以及反射機(jī)制創(chuàng)建蔗坯,但是無論是new還是反射的形式創(chuàng)建都會調(diào)用對象的構(gòu)造方法來完成對象的初始化康震,而Unsafe類提供創(chuàng)建對象實(shí)例新的途徑如下:
//傳入一個對象的class并創(chuàng)建該實(shí)例對象,但不會調(diào)用構(gòu)造方法
public native Object allocateInstance(Class cls) throws InstantiationException;
2.3. 類宾濒、實(shí)例對象以及變量操作:Unsafe類提供類腿短、實(shí)例對象以及變量操縱方法
//獲取字段f在實(shí)例對象中的偏移量
public native long objectFieldOffset(Field f);
//靜態(tài)屬性的偏移量,用于在對應(yīng)的Class對象中讀寫靜態(tài)屬性
public native long staticFieldOffset(Field f);
//返回值就是f.getDeclaringClass()
public native Object staticFieldBase(Field f);
//獲得給定對象偏移量上的int值绘梦,所謂的偏移量可以簡單理解為指針指向該變量的內(nèi)存地址橘忱,
//通過偏移量便可得到該對象的變量,進(jìn)行各種操作
public native int getInt(Object o, long offset);
//設(shè)置給定對象上偏移量的int值
public native void putInt(Object o, long offset, int x);
//獲得給定對象偏移量上的引用類型的值
public native Object getObject(Object o, long offset);
//設(shè)置給定對象偏移量上的引用類型的值
public native void putObject(Object o, long offset, Object x);
//其他基本數(shù)據(jù)類型(long,char,byte,float,double)的操作與getInthe及putInt相同
//設(shè)置給定對象的int值卸奉,使用volatile語義钝诚,即設(shè)置后立馬更新到內(nèi)存對其他線程可見
public native void putIntVolatile(Object o, long offset, int x);
//獲得給定對象的指定偏移量offset的int值,使用volatile語義榄棵,總能獲取到最新的int值凝颇。
public native int getIntVolatile(Object o, long offset);
//其他基本數(shù)據(jù)類型(long,char,byte,float,double)的操作與putIntVolatile
//及getIntVolatile相同,引用類型putObjectVolatile也一樣疹鳄。
..........省略代碼
//與putIntVolatile一樣拧略,但要求被操作字段必須有volatile修飾
public native void putOrderedInt(Object o,long offset,int x);
下面通過一個小Demo來加深大家對與Unsafe類的熟悉程度:
Unsafe類是沒有對外提供構(gòu)造函數(shù)的,雖然Unsafe類對外提供getUnsafe()方法瘪弓,但該方法只提供給Bootstrap類加載器使用垫蛆,普通用戶調(diào)用將拋出異常,所以我們在下面的Demo中使用反射技術(shù)獲取了Unsafe實(shí)例對象并進(jìn)行相關(guān)操作腺怯。
public static Unsafe getUnsafe() {
Class cc = sun.reflect.Reflection.getCallerClass(2);
if (cc.getClassLoader() != null)
throw new SecurityException("Unsafe");
return theUnsafe;
}
public class UnSafeDemo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
// 通過反射得到theUnsafe對應(yīng)的Field對象
Field field = Unsafe.class.getDeclaredField("theUnsafe");
// 設(shè)置該Field為可訪問
field.setAccessible(true);
// 通過Field得到該Field對應(yīng)的具體對象月褥,傳入null是因?yàn)樵揊ield為static的
Unsafe unsafe = (Unsafe) field.get(null);
System.out.println(unsafe);
//通過allocateInstance直接創(chuàng)建對象
Demo demo = (Demo) unsafe.allocateInstance(Demo.class);
Class demoClass = demo.getClass();
Field str = demoClass.getDeclaredField("str");
Field i = demoClass.getDeclaredField("i");
Field staticStr = demoClass.getDeclaredField("staticStr");
//獲取實(shí)例變量str和i在對象內(nèi)存中的偏移量并設(shè)置值
unsafe.putInt(demo,unsafe.objectFieldOffset(i),1);
unsafe.putObject(demo,unsafe.objectFieldOffset(str),"Hello Word!");
// 返回 User.class
Object staticField = unsafe.staticFieldBase(staticStr);
System.out.println("staticField:" + staticStr);
//獲取靜態(tài)變量staticStr的偏移量staticOffset
long staticOffset = unsafe.staticFieldOffset(userClass.getDeclaredField("staticStr"));
//獲取靜態(tài)變量的值
System.out.println("設(shè)置前的Static字段值:"+unsafe.getObject(staticField,staticOffset));
//設(shè)置值
unsafe.putObject(staticField,staticOffset,"Hello Java!");
//再次獲取靜態(tài)變量的值
System.out.println("設(shè)置后的Static字段值:"+unsafe.getObject(staticField,staticOffset));
//調(diào)用toString方法
System.out.println("輸出結(jié)果:"+demo.toString());
long data = 1000;
byte size = 1; //單位字節(jié)
//調(diào)用allocateMemory分配內(nèi)存,并獲取內(nèi)存地址memoryAddress
long memoryAddress = unsafe.allocateMemory(size);
//直接往內(nèi)存寫入數(shù)據(jù)
unsafe.putAddress(memoryAddress, data);
//獲取指定內(nèi)存地址的數(shù)據(jù)
long addrData = unsafe.getAddress(memoryAddress);
System.out.println("addrData:"+addrData);
/**
* 輸出結(jié)果:
sun.misc.Unsafe@0f18aef2
staticField:class com.demo.Demo
設(shè)置前的Static字段值:Demo_Static
設(shè)置后的Static字段值:Hello Java!
輸出USER:Demo{str='Hello Word!', i='1', staticStr='Hello Java!'}
addrData:1000
*/
}
}
class Demo{
public Demo(){
System.out.println("我是Demo類的構(gòu)造函數(shù),我被人調(diào)用創(chuàng)建對象實(shí)例啦....");
}
private String str;
private int i;
private static String staticStr = "Demo_Static";
@Override
public String toString() {
return "Demo{" +
"str = '" + str + '\'' +
", i = '" + i +'\'' +
", staticStr = " + staticStr +'\'' +
'}';
}
}
2.4瓢喉、數(shù)組操作:Unsafe類提供直接獲取數(shù)組元素內(nèi)存位置的途徑
//獲取數(shù)組第一個元素的偏移地址
public native int arrayBaseOffset(Class arrayClass);
//數(shù)組中一個元素占據(jù)的內(nèi)存空間,arrayBaseOffset與arrayIndexScale配合使用宁赤,可定位數(shù)組中每個元素在內(nèi)存中的位置
public native int arrayIndexScale(Class arrayClass);
2.4、CAS相關(guān)操作:Unsafe類提供Java中的CAS操作支持
//第一個參數(shù)o為給定對象栓票,offset為對象內(nèi)存的偏移量决左,通過這個偏移量迅速定位字段并設(shè)置或獲取該字段的值愕够,
//expected表示期望值,x表示要設(shè)置的值佛猛,下面3個方法都通過CAS原子指令執(zhí)行操作惑芭。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);
2.5、JDK8之后新增的基于原有CAS方法的方法
//1.8新增继找,給定對象o遂跟,根據(jù)獲取內(nèi)存偏移量指向的字段,將其增加delta婴渡,
//這是一個CAS操作過程幻锁,直到設(shè)置成功方能退出循環(huán),返回舊值
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
//獲取內(nèi)存中最新值
v = getIntVolatile(o, offset);
//通過CAS操作
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
//1.8新增边臼,方法作用同上哄尔,只不過這里操作的long類型數(shù)據(jù)
public final long getAndAddLong(Object o, long offset, long delta) {
long v;
do {
v = getLongVolatile(o, offset);
} while (!compareAndSwapLong(o, offset, v, v + delta));
return v;
}
//1.8新增,給定對象o柠并,根據(jù)獲取內(nèi)存偏移量對于字段岭接,將其 設(shè)置為新值newValue,
//這是一個CAS操作過程臼予,直到設(shè)置成功方能退出循環(huán)鸣戴,返回舊值
public final int getAndSetInt(Object o, long offset, int newValue) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, newValue));
return v;
}
// 1.8新增,同上粘拾,操作的是long類型
public final long getAndSetLong(Object o, long offset, long newValue) {
long v;
do {
v = getLongVolatile(o, offset);
} while (!compareAndSwapLong(o, offset, v, newValue));
return v;
}
//1.8新增葵擎,同上,操作的是引用類型數(shù)據(jù)
public final Object getAndSetObject(Object o, long offset, Object newValue) {
Object v;
do {
v = getObjectVolatile(o, offset);
} while (!compareAndSwapObject(o, offset, v, newValue));
return v;
}
如上方法如果有伙伴看過JDK8的Atomic包的源碼應(yīng)該也能從中見到它們的身影半哟。
2.6酬滤、線程操作:Unsafe類提供線程掛起與恢復(fù)操作支持
將一個線程進(jìn)行掛起是通過park方法實(shí)現(xiàn)的,調(diào)用park后寓涨,線程將一直阻塞直到超時或者中斷等條件出現(xiàn)盯串。unpark可以終止一個掛起的線程,使其恢復(fù)正常戒良。Java對線程的掛起操作被封裝在LockSupport類中体捏,LockSupport類中有各種版本pack方法,其底層實(shí)現(xiàn)最終還是使用Unsafe.park()方法和Unsafe.unpark()方法來實(shí)現(xiàn)糯崎。
//線程調(diào)用該方法几缭,線程將一直阻塞直到超時,或者是中斷條件出現(xiàn)沃呢。
public native void park(boolean isAbsolute, long time);
//終止掛起的線程年栓,恢復(fù)正常.java.util.concurrent包中掛起操作都是在LockSupport類實(shí)現(xiàn)的,其底層正是使用這兩個方法薄霜,
public native void unpark(Object thread);
2.7某抓、內(nèi)存屏障:在之前文章中提到過的JMM纸兔、指令重排序等操作的內(nèi)存屏障定義支持
//在該方法之前的所有讀操作,一定在load屏障之前執(zhí)行完成
public native void loadFence();
//在該方法之前的所有寫操作否副,一定在store屏障之前執(zhí)行完成
public native void storeFence();
//在該方法之前的所有讀寫操作汉矿,一定在full屏障之前執(zhí)行完成,這個內(nèi)存屏障相當(dāng)于上面兩個的合體功能
public native void fullFence();
2.8备禀、其他操作:更多操作請參考JDK8官方API文檔
//獲取持有鎖洲拇,已不建議使用
@Deprecated
public native void monitorEnter(Object var1);
//釋放鎖,已不建議使用
@Deprecated
public native void monitorExit(Object var1);
//嘗試獲取鎖曲尸,已不建議使用
@Deprecated
public native boolean tryMonitorEnter(Object var1);
//獲取本機(jī)內(nèi)存的頁數(shù)赋续,這個值永遠(yuǎn)都是2的冪次方
public native int pageSize();
//告訴虛擬機(jī)定義了一個沒有安全檢查的類,默認(rèn)情況下這個類加載器和保護(hù)域來著調(diào)用者類
public native Class defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);
//加載一個匿名類
public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches);
//判斷是否需要加載一個類
public native boolean shouldBeInitialized(Class<?> c);
//確保類一定被加載
public native void ensureClassInitialized(Class<?> c);
三队腐、J.U.C包下的原子操作包Atomic
通過前面部分的分析我們應(yīng)該基本理解CAS無鎖思想以及對魔法類Unsafe有較全面的認(rèn)知,而這些也是我們分析atomic包的基本條件奏篙,那么我們接下來再一步步分析CAS在Java中的應(yīng)用柴淘。JDK5之后推出的JUC并發(fā)包中提供了java.util.concurrent.atomic原子包,在該包下提供了大量基于CAS實(shí)現(xiàn)的原子操作類秘通,如以后不想對程序代碼加鎖但仍然想避免線程安全問題为严,那么便可以使用該包下提供的類,原子包提供的操作類主要可分為如下四種類型:
- 基本類型原子操作類
- 引用類型原子操作類
- 數(shù)組類型原子操作類
- 屬性更新原子操作類
3.1肺稀、基本類型原子操作類
Atomic包中提供的對于基本類型的原子操作類分別為AtomicInteger第股、AtomicBoolean、AtomicLong三個话原,它們的底層實(shí)現(xiàn)方式及使用方式是一致的夕吻,所以我們只分析其中一個AtomicInteger用作舉例,AtomicInteger主要是針對int類型的數(shù)據(jù)執(zhí)行原子操作繁仁,它提供了原子自增方法涉馅、原子自減方法以及原子賦值方法等API:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// 獲取指針類Unsafe類實(shí)例
private static final Unsafe unsafe = Unsafe.getUnsafe();
//變量value在AtomicInteger實(shí)例對象內(nèi)的內(nèi)存偏移量
private static final long valueOffset;
static {
try {
//通過unsafe類的objectFieldOffset()方法,獲取value變量在對象內(nèi)存中的偏移
//通過該偏移量valueOffset黄虱,unsafe類的內(nèi)部方法可以獲取到變量value對其進(jìn)行取值或賦值操作
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//當(dāng)前AtomicInteger封裝的int變量值 value
private volatile int value;
public AtomicInteger(int initialValue) {
value = initialValue;
}
public AtomicInteger() {
}
//獲取當(dāng)前最新值稚矿,
public final int get() {
return value;
}
//設(shè)置當(dāng)前值,具備volatile效果捻浦,方法用final修飾是為了更進(jìn)一步的保證線程安全
public final void set(int newValue) {
value = newValue;
}
//最終會設(shè)置成newValue晤揣,使用該方法后可能導(dǎo)致其他線程在之后的一小段時間內(nèi)可以獲取到舊值,有點(diǎn)類似于延遲加載
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}
//設(shè)置新值并獲取舊值朱灿,底層調(diào)用的是CAS操作即unsafe.compareAndSwapInt()方法
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
//如果當(dāng)前值為expect昧识,則設(shè)置為update(當(dāng)前值指的是value變量)
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//當(dāng)前值自增加1,返回舊值盗扒,底層CAS操作
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//當(dāng)前值自減扣1滞诺,返回舊值形导,底層CAS操作
public final int getAndDecrement() {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
//當(dāng)前值增加delta,返回舊值习霹,底層CAS操作
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
//當(dāng)前值自增加1并返回自增后的新值朵耕,底層CAS操作
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
//當(dāng)前值自減扣1并返回自減之后的新值,底層CAS操作
public final int decrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}
//當(dāng)前值增加delta淋叶,返回新值阎曹,底層CAS操作
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
//省略一些不常用的方法....
}
從上面的源碼中我們可以得知,AtomicInteger原子類的所有方法中并沒有使用到任何互斥鎖機(jī)制來實(shí)現(xiàn)同步煞檩,而是通過我們前面介紹的Unsafe類提供的CAS操作來保障的線程安全处嫌,在我們前面關(guān)于Synchronized原理分析的文章中講到過i++其實(shí)存在線程安全問題,那么我們在來觀察AtomicInteger原子類中的自增實(shí)現(xiàn)incrementAndGet:
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
從如上源碼我們可以看到最終的實(shí)現(xiàn)是調(diào)用了unsafe.getAndAddInt()方法來完成的斟湃,這個方法在我們前面的分析中得知是JDK8之后基于原有cas操作新增的方法熏迹,而我們進(jìn)一步跟進(jìn):
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
可看出getAndAddInt通過一個do while死循環(huán)不斷的重試更新要設(shè)置的值,直到成功為止凝赛,調(diào)用的是Unsafe類中的compareAndSwapInt方法注暗,是一個CAS操作方法。注意:Java8之前的源碼實(shí)現(xiàn)中是for循環(huán)墓猎,而且是直接在AtomicInteger類中實(shí)現(xiàn)的自增邏輯捆昏,在Java8中被換成了while以及被挪動到是Unsafe類中實(shí)現(xiàn)。
下面來通過一個簡單的小demo掌握一下基本類型的原子操作類用法:
public class AtomicIntegerDemo {
// 創(chuàng)建共享變量 atomicI
static AtomicInteger atomicI = new AtomicInteger();
public static class AddThread implements Runnable{
public void run(){
for(int i = 0; i < 10000; i++)
atomicI.incrementAndGet();
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
//開啟5條線程同時執(zhí)行atomicI的自增操作
for(int i = 1; i <= 5; i++){
threads[i]=new Thread(new AddThread());
}
//啟動線程
for(int i = 1; i <= 5; i++){threads[i].start();}
for(int i = 1; i <= 5; i++){threads[i].join();}
System.out.println(atomicI);
}
}
//輸出結(jié)果:50000
在上述demo中使用AtomicInteger代替原本的int毙沾,這樣便能做到不加鎖也能保證線程安全骗卜,而AtomicBoolean以及AtomicLong將不再分析,使用以及原理都是一樣的左胞。
3.2寇仓、引用類型原子操作類
引用類型的原子操作類主要分析AtomicReference類,其他的原理及使用都是一致的烤宙,后續(xù)內(nèi)容不再重復(fù)闡述這段話焚刺,先來看一個demo:
public class AtomicReferenceDemo {
public static AtomicReference<Student> atomicStudentRef = new AtomicReference<Student>();
public static void main(String[] args) {
Student s1 = new Student(1, "竹子");
atomicStudentRef.set(s1);
Student s2 = new Student(2, "熊貓");
atomicStudentRef.compareAndSet(s1, s2);
System.out.println(atomicStudentRef.get().toString());
//執(zhí)行結(jié)果:Student{id=2, name="熊貓"}
}
static class Student {
private int id;
public String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Student{" +"id=" + id +", name=" + name +"}";
}
}
}
在此之前我們分析了原子計(jì)數(shù)器AtomicInteger類的底層實(shí)現(xiàn)是通過Unsafe類中CAS操作來實(shí)現(xiàn)的,那么我們現(xiàn)在的AtomicReference底層又是怎么實(shí)現(xiàn)的呢门烂?
public class AtomicReference<V> implements java.io.Serializable {
// 得到unsafe對象實(shí)例
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 定義值偏移量
private static final long valueOffset;
static {
try {
/* 靜態(tài)代碼塊在類加載時為偏移量賦值乳愉,通過unsafe類提供的得到類屬性的
地址API得到當(dāng)前類定義的屬性value的地址 */
valueOffset = unsafe.objectFieldOffset
(AtomicReference.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//內(nèi)部變量value,Unsafe類通過valueOffset內(nèi)存偏移量即可獲取該變量
private volatile V value;
/* 原子替換方法屯远,間接調(diào)用Unsafe類的compareAndSwapObject(),
它是一個實(shí)現(xiàn)了CAS操作的本地(native)方法 */
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
//設(shè)置并獲取舊值
public final V getAndSet(V newValue) {
return (V)unsafe.getAndSetObject(this, valueOffset, newValue);
}
//省略代碼......
}
//Unsafe類中的getAndSetObject方法蔓姚,實(shí)際調(diào)用還是CAS操作
public final Object getAndSetObject(Object o, long offset, Object newValue) {
Object v;
do {
v = getObjectVolatile(o, offset);
} while (!compareAndSwapObject(o, offset, v, newValue));
return v;
}
從上述源碼中我們可以得知,AtomicReference與我們前面分析的AtomicInteger實(shí)現(xiàn)的原理大致相同慨丐,最終都是通過Unsafe類中提供的CAS操作來實(shí)現(xiàn)的坡脐,關(guān)于AtomicReference的其他方法實(shí)現(xiàn)原理也大致相同,只不過需要注意的是Java8為AtomicReference新增了幾個API:
- getAndUpdate(UnaryOperator<V>)
- updateAndGet(UnaryOperator<V>)
- getAndAccumulate(V,AnaryOperator<V>)
- accumulateAndGet(V,AnaryOperator<V>)
上述的方法幾乎存在于所有的原子類中房揭,而這些方法可以基于Lambda表達(dá)式對傳遞進(jìn)來的期望值或要更新的值進(jìn)行其他操作后再進(jìn)行CAS操作备闲,說簡單一些就是對期望值或要更新的值進(jìn)行額外修改后再執(zhí)行CAS更新晌端。
3.3、數(shù)組類型原子操作類
我們之前在剛開始接觸Java學(xué)習(xí)的數(shù)組恬砂,在我們操作其中元素時都會存在線程安全問題咧纠,而我們數(shù)組類型原子操作類的含義其實(shí)非常簡單,指的就是利用原子的形式更新數(shù)組中的元素從而避免出現(xiàn)線程安全問題泻骤。而JUC包中的數(shù)組類型原子操作類具體分為以下三個類:
- AtomicIntegerArray:原子更新整數(shù)數(shù)組里的元素
- AtomicLongArray:原子更新長整數(shù)數(shù)組里的元素
- AtomicReferenceArray:原子更新引用類型數(shù)組里的元素
同樣的我們分析也只分析單個類漆羔,其他兩個大致相同,此處已AtomicIntegerArray舉例分析狱掂,先上一個Demo案例:
public class AtomicIntegerArrayDemo {
static AtomicIntegerArray atomicArr = new AtomicIntegerArray(5);
public static class incrementTask implements Runnable{
public void run(){
// 執(zhí)行數(shù)組中元素自增操作,參數(shù)為index,即數(shù)組下標(biāo)
for(int i = 0; i < 10000; i++) atomicArr.getAndIncrement(i % atomicArr.length());
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[5];
for(int i = 0; i < 5;i++)
threads[i] = new Thread(new incrementTask());
for(int i = 0; i < 5;i++) threads[i].start();
for(int i = 0; i < 5;i++) threads[i].join();
System.out.println(atomicArr);
/* 執(zhí)行結(jié)果:
[10000, 10000, 10000, 10000, 10000]
*/
}
}
如上Demo中我們啟動了五條線程對數(shù)組中的元素進(jìn)行自增操作演痒,執(zhí)行的結(jié)果符合我們的預(yù)期。那么我們接下來再一步步的去分析AtomicIntegerArray內(nèi)部的實(shí)現(xiàn)邏輯趋惨,源碼如下:
public class AtomicIntegerArray implements java.io.Serializable {
//獲取Unsafe實(shí)例對象
private static final Unsafe unsafe = Unsafe.getUnsafe();
//從前面我們分析Unsafe類得知arrayBaseOffset()作用:獲取數(shù)組的第一個元素內(nèi)存起始地址
private static final int base = unsafe.arrayBaseOffset(int[].class);
private static final int shift;
//內(nèi)部數(shù)組
private final int[] array;
static {
//獲取數(shù)組中一個元素占據(jù)的內(nèi)存空間
int scale = unsafe.arrayIndexScale(int[].class);
//判斷是否為2的次冪鸟顺,一般為2的次冪否則拋異常
if ((scale & (scale - 1)) != 0)
throw new Error("data type scale not a power of two");
//
shift = 31 - Integer.numberOfLeadingZeros(scale);
}
private long checkedByteOffset(int i) {
if (i < 0 || i >= array.length)
throw new IndexOutOfBoundsException("index " + i);
return byteOffset(i);
}
//計(jì)算數(shù)組中每個元素的的內(nèi)存地址
private static long byteOffset(int i) {
return ((long) i << shift) + base;
}
//省略代碼......
}
在我們前面的Unsafe分析中得知arrayBaseOffset可以獲取一個數(shù)組中的第一個元素內(nèi)存起始位置,而我們還有另一個API:arrayIndexScale則可以得知數(shù)組中指定下標(biāo)元素的內(nèi)存占用空間大小器虾,而目前的是AtomicIntegerArray是int讯嫂,在我們學(xué)習(xí)Java基礎(chǔ)的時候得知int的大小在內(nèi)存中是占用4Byte(字節(jié)),所以scale的值為4曾撤。那么現(xiàn)在如果根據(jù)這兩個信息計(jì)算數(shù)組中每個元素的內(nèi)存起始地址呢端姚?我們稍作推導(dǎo)即可得出如下結(jié)論:
數(shù)組每個元素起始內(nèi)存位置 = 數(shù)組第一個元素起始位置 + 數(shù)組元素下標(biāo) * 數(shù)組中每個元素占用的內(nèi)存空間大小
而上述源碼中的byteOffset(i)方法的則是如上闡述公式的體現(xiàn)晕粪。有人可能會看到這里有些疑惑挤悉,該方法的實(shí)現(xiàn)似乎與我剛剛描述的結(jié)論有出入。別急巫湘,我們先來看看shift這個值是怎么得來的:
shift = 31 - Integer.numberOfLeadingZeros(scale);
Integer.numberOfLeadingZeros(scale):計(jì)算出scale的前導(dǎo)零個數(shù)(轉(zhuǎn)換為二進(jìn)制后前面連續(xù)的0數(shù)量稱為前零導(dǎo)數(shù))装悲,scale=4,轉(zhuǎn)成二進(jìn)制為00000000 00000000 00000000 00000100尚氛,所以前零導(dǎo)數(shù)為29诀诊,所以shift值則為2。
那么我們最開始的問題:數(shù)組中每個元素的起始內(nèi)存位置怎么計(jì)算呢阅嘶?我們利用我們的剛剛得到的shift值套入byteOffset(i)方法計(jì)算數(shù)組中每個元素的起始內(nèi)存位置(數(shù)組下標(biāo)不越界情況下):
byteOffset方法體:(i << shift) + base(i為index即數(shù)組元素下標(biāo)属瓣,base為數(shù)組第一個元素起始內(nèi)存位置)
第一個元素:memoryAddress = 0 << 2 + base 即 memoryAddress = base + 0 * 4
第二個元素:memoryAddress = 1 << 2 + base 即 memoryAddress = base + 1 * 4
第三個元素:memoryAddress = 2 << 2 + base 即 memoryAddress = base + 2 * 4
第四個元素:memoryAddress = 3 << 2 + base 即 memoryAddress = base + 3 * 4
其他元素位置以此類推.......
如上闡述則是AtomicIntegerArray.byteOffset方法原理。所以byteOffset(int)方法可以根據(jù)數(shù)組下標(biāo)計(jì)算出每個元素的內(nèi)存地址讯柔。而AtomicIntegerArray中的其他方法都是間接調(diào)用Unsafe類的CAS原子操作方法實(shí)現(xiàn)抡蛙,如下簡單看其中幾個常用方法:
//執(zhí)行自增操作,返回舊值魂迄,入?yún)是index即數(shù)組元素下標(biāo)
public final int getAndIncrement(int i) {
return getAndAdd(i, 1);
}
//指定下標(biāo)元素執(zhí)行自增操作粗截,并返回新值
public final int incrementAndGet(int i) {
return getAndAdd(i, 1) + 1;
}
//指定下標(biāo)元素執(zhí)行自減操作,并返回新值
public final int decrementAndGet(int i) {
return getAndAdd(i, -1) - 1;
}
//間接調(diào)用unsafe.getAndAddInt()方法
public final int getAndAdd(int i, int delta) {
return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
}
//Unsafe類中的getAndAddInt方法捣炬,執(zhí)行CAS操作
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
AtomicReferenceArray與AtomicLongArray原理則不再闡述熊昌。
3.4绽榛、屬性更新原子操作類
如果我們只需要一個類的某個字段(類屬性)也變?yōu)樵硬僮骷醋屇硞€普通變量也變?yōu)樵硬僮鳎梢允褂脤傩愿略硬僮黝愋鲆伲缭谀承r候項(xiàng)目需求發(fā)生變化灭美,使得某個類中之前不需要涉及多線程操作的某個屬性需要執(zhí)行多線程操作,由于在編碼時該屬性被多處使用选泻,改動起來代碼侵入性比較高并且比較復(fù)雜冲粤,而且之前的code中使用該屬性的地方無需考慮線程安全,只要求新場景需要保證時页眯,可以借助原子更新器處理這種場景梯捕,J.U.C.Atomic并發(fā)原子包提供了如下三個類:
- AtomicIntegerFieldUpdater:更新整型的屬性的原子操作類
- AtomicLongFieldUpdater:更新長整型屬性的原子操作類
- AtomicReferenceFieldUpdater:更新引用類型中屬性的原子操作類
不過值得令人注意的是:使用原子更新類的條件是比較苛刻的,如下:
- 操作的字段不能被static修飾
- 操作的字段不能被final修飾窝撵,因?yàn)槌A繜o法修改
- 操作的字段必須被volatile修飾保證可見性傀顾,也就是需要保證數(shù)據(jù)的讀取是線程可見的
- 屬性必須對當(dāng)前的Updater所在的區(qū)域是可見的,如果不是當(dāng)前類內(nèi)部進(jìn)行原子更新器操作不能使用private碌奉,protected修飾符短曾。子類操作父類時修飾符必須是protected權(quán)限及以上,如果在同一個package下則必須是default權(quán)限及以上赐劣,也就是說無論何時都應(yīng)該保證操作類與被操作類間的可見性嫉拐。
來個簡單的小Demo感受一下:
public class AtomicIntegerFieldUpdaterDemo{
// 定義更新整型的屬性的原子操作類,目標(biāo)屬性:Course.courseScore
static AtomicIntegerFieldUpdater<Course> courseAIFU =
AtomicIntegerFieldUpdater.newUpdater(Course.class, "courseScore");
// 定義更新引用類型中屬性的原子操作類魁兼,目標(biāo)屬性:Student.studentName
static AtomicReferenceFieldUpdater<Student,String> studentARFU =
AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"studentName");
// 定義原子計(jì)數(shù)器效驗(yàn)數(shù)據(jù)準(zhǔn)確性
public static AtomicInteger courseScoreCount = new AtomicInteger(0);
public static void main(String[] args) throws Exception{
final Course course = new Course();
Thread[] threads = new Thread[1000];
for(int i = 0;i < 1000;i++){
threads[i] = new Thread(()->{
if(Math.random()>0.6){
courseAIFU.incrementAndGet(course);
courseScoreCount.incrementAndGet();
}
});
threads[i].start();
}
for(int i = 0;i < 1000;i++) threads[i].join();
System.out.println("Course.courseScore:" + course.courseScore);
System.out.println("數(shù)據(jù)效驗(yàn)結(jié)果:" + courseScoreCount.get());
// 更新引用類型中屬性的原子操作類的demo
Student student = new Student(1,"竹子");
studentARFU.compareAndSet(student,student.studentName,"熊貓");
System.out.println(student.toString());
}
public static class Course{
int courseId;
String courseName;
volatile int courseScore;
}
public static class Student{
int studentId;
volatile String studentName;
public Student(int studentId,String studentName){
this.studentId = studentId;
this.studentName = studentName;
}
@Override
public String toString() {
return "Student[studentId="+studentId+"studentName="+studentName+"]";
}
}
}
/** 運(yùn)行結(jié)果:
* Course.courseScore:415
* 數(shù)據(jù)效驗(yàn)結(jié)果:415
* Student[studentId=1studentName=熊貓]
**/
我們從上面的代碼中婉徘,存在兩個內(nèi)部靜態(tài)類:Course以及Student,我們使用AtomicIntegerFieldUpdater作用在Course.courseScore屬性上咐汞,使用AtomicReferenceFieldUpdater作用在Student.studentName上盖呼。在上面的代碼中,我們開啟一千條線程進(jìn)行邏輯操作化撕,使用AtomicInteger定義計(jì)數(shù)器courseScoreCount進(jìn)行數(shù)據(jù)效驗(yàn)几晤,其目的在于效驗(yàn)AtomicIntegerFieldUpdater是否能保證線程安全問題。當(dāng)我們開啟的線程隨機(jī)出的數(shù)字大于0.6時代表成績合格植阴,此時將這次的成績錄入進(jìn)Course.courseScore中蟹瘾,然后再對courseScoreCount進(jìn)行自增方便后續(xù)進(jìn)行數(shù)據(jù)效驗(yàn)。最終結(jié)果輸出Course.courseScore與courseScoreCount結(jié)果一致掠手,也就代表著AtomicIntegerFieldUpdater能夠正確的更新Course.courseScore值能夠保證線程安全問題憾朴。而對于AtomicReferenceFieldUpdater我們在上面的代碼中簡單的編寫了demo演示了其使用方式,我們在使用AtomicReferenceFieldUpdater的時候值得注意的一點(diǎn)就是需要傳入兩個泛型參數(shù)惨撇,一個是修改的類的class對象伊脓,一個是修改的Field的class對象。至于另外的AtomicLongFieldUpdater則不再演示,使用方式與AtomicIntegerFieldUpdater大致相同报腔。至此株搔,我們再研究一下AtomicIntegerFieldUpdater的實(shí)現(xiàn)原理,先上代碼:
public abstract class AtomicIntegerFieldUpdater<T> {
public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass,
String fieldName){
return new AtomicIntegerFieldUpdaterImpl<U>
(tclass, fieldName, Reflection.getCallerClass());
}
}
從源碼中不難發(fā)現(xiàn)纯蛾,其實(shí)AtomicIntegerFieldUpdater的定義是一個抽象類纤房,最終實(shí)現(xiàn)類為AtomicIntegerFieldUpdaterImpl,進(jìn)一步跟進(jìn)AtomicIntegerFieldUpdaterImpl:
private static class AtomicIntegerFieldUpdaterImpl<T>
extends AtomicIntegerFieldUpdater<T> {
// 獲取unsafe實(shí)例
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 定義內(nèi)存偏移量
private final long offset;
private final Class<T> tclass;
private final Class<?> cclass;
// 構(gòu)造方法
AtomicIntegerFieldUpdaterImpl(final Class<T> tclass,
final String fieldName,
final Class<?> caller) {
final Field field;// 要修改的字段
final int modifiers;// 字段修飾符
try {
field = AccessController.doPrivileged(
new PrivilegedExceptionAction<Field>() {
public Field run() throws NoSuchFieldException {
// 通過反射獲取字段對象
return tclass.getDeclaredField(fieldName);
}
});
// 獲取字段修飾符
modifiers = field.getModifiers();
//對字段的訪問權(quán)限進(jìn)行檢查,不在訪問范圍內(nèi)拋異常
sun.reflect.misc.ReflectUtil.ensureMemberAccess(
caller, tclass, null, modifiers);
// 得到對應(yīng)類對象的類加載器
ClassLoader cl = tclass.getClassLoader();
ClassLoader ccl = caller.getClassLoader();
if ((ccl != null) && (ccl != cl) &&
((cl == null) || !isAncestor(cl, ccl))) {
sun.reflect.misc.ReflectUtil.checkPackageAccess(tclass);
}
} catch (PrivilegedActionException pae) {
throw new RuntimeException(pae.getException());
} catch (Exception ex) {
throw new RuntimeException(ex);
}
Class<?> fieldt = field.getType();
// 判斷是否為int類型
if (fieldt != int.class)
throw new IllegalArgumentException("Must be integer type");
// 判斷是否被volatile修飾
if (!Modifier.isVolatile(modifiers))
throw new IllegalArgumentException("Must be volatile type");
this.cclass = (Modifier.isProtected(modifiers) &&
caller != tclass) ? caller : null;
this.tclass = tclass;
// 獲取該字段的在對象內(nèi)存的偏移量翻诉,通過內(nèi)存偏移量可以獲取或者修改該字段的值
offset = unsafe.objectFieldOffset(field);
}
}
從AtomicIntegerFieldUpdaterImpl源碼中不難看出其實(shí)字段更新器都是通過反射機(jī)制+Unsafe類實(shí)現(xiàn)的炮姨,看看AtomicIntegerFieldUpdaterImpl中自增方法incrementAndGet實(shí)現(xiàn):
public int incrementAndGet(T obj) {
int prev, next;
do {
prev = get(obj);
next = prev + 1;
// 調(diào)用下面的compareAndSet方法完成cas操作
} while (!compareAndSet(obj, prev, next));
return next;
}
// 最終調(diào)用的還是Unsafe類中compareAndSwapInt()方法完成
public boolean compareAndSet(T obj, int expect, int update) {
if (obj == null || obj.getClass() != tclass || cclass != null) fullCheck(obj);
return unsafe.compareAndSwapInt(obj, offset, expect, update);
}
我們到這可以發(fā)現(xiàn),其實(shí)關(guān)于J.U.C中的Atomic原子包中原子類的最終實(shí)現(xiàn)都是通過我們前面分析的Unsafe類來完成的碰煌,包括我們之后會談到的很多并發(fā)知識舒岸,如AQS等都會牽扯到CAS機(jī)制。
四芦圾、CAS無鎖存在的問題
4.1蛾派、CAS的ABA問題
當(dāng)?shù)谝粋€線程執(zhí)行CAS(V,E,N)操作,在獲取到當(dāng)前變量V个少,準(zhǔn)備修改為新值N前洪乍,另外兩個線程已連續(xù)修改了兩次變量V的值,使得該值又恢復(fù)為第一個線程看到的原有值夜焦,這樣的話壳澳,我們就無法正確判斷這個變量是否已被修改過,簡單舉例:線程T1,T2,T3茫经,共享變量atomicI=0巷波,當(dāng)?shù)谝粋€線程T1準(zhǔn)備將atomicI更改為1時,此時T1線程看到的值是1并將atomicI=1讀取回工作內(nèi)存科平,然而在T1線程進(jìn)行操作的過程中褥紫,T2線程將值更新為了1姜性,然后T3線程又將atomicI更改成了0瞪慧,此時T1再回來進(jìn)行更新的時候是無法得知這個值已經(jīng)被其他線程更改過的,在做V==E判斷時發(fā)現(xiàn)atomicI還是原先的值部念,就會對atomicI進(jìn)行更改操作弃酌,但是此時的現(xiàn)場與T1線程第一次看到的值時的現(xiàn)場不同了。如下圖:
如上這個例子就是CAS機(jī)制存在的問題儡炼,一般情況下發(fā)生的幾率很小妓湘,而且就算發(fā)生了一般業(yè)務(wù)也不會造成什么問題,比如上面這個例子乌询,就算出現(xiàn)了ABA問題也不會造成影響榜贴,因?yàn)椴粫绊憯?shù)據(jù)的最終一致性,T1線程的預(yù)期值本身就為1妹田,就算出現(xiàn)了ABA問題唬党,T1線程更新后的值還是為1鹃共。但是在某些情況下還是需要避免此類問題的,比如一個單向鏈表實(shí)現(xiàn)的堆棧stack如下:
如上圖所示驶拱,此時棧頂元素為A霜浴,此時線程T1已經(jīng)知道A.next為元素B,希望通過CAS機(jī)制將棧頂元素替換為B蓝纲,執(zhí)行如下:
stackHead.compareAndSet(A,B);
在T1線程執(zhí)行這行代碼時線程T2介入阴孟,線程T2將A、B出棧税迷,然后再將元素Y永丝、X、A入棧箭养,此時元素Y类溢、X、A處于棧內(nèi)露懒,元素B處于孤立狀態(tài)闯冷,示意圖如下:
正好此時線程T1執(zhí)行CAS寫回操作,檢測發(fā)現(xiàn)棧頂仍為A懈词,所以CAS成功停蕉,棧頂變?yōu)锽,但實(shí)際上B.next為null锨并,所以此時的情況變?yōu)椋?br>
其中堆棧中只有B元素桦锄,Y、X元素不再存在于堆棧中抠忘,但是確實(shí)T2線程是執(zhí)行了Y撩炊、X入棧操作的,但是卻平白無故的將X崎脉、Y丟掉了拧咳。
總而言之,只要在場景中存在需要基于動態(tài)變化而要做出的操作囚灼,ABA問題的出現(xiàn)就需要解決骆膝,但是如果你的應(yīng)用只停留在數(shù)據(jù)表面得到的結(jié)果而做的判斷,那么ABA問題你就可以不用去關(guān)注灶体。
4.2阅签、CAS的ABA問題的解決方案
那么當(dāng)我們出現(xiàn)這類問題并且需要我們?nèi)リP(guān)注時又該如何解決呢?在我們通過其他形式去實(shí)現(xiàn)樂觀鎖時通常會通過version版本號來進(jìn)行標(biāo)記從而避免并發(fā)帶來的問題蝎抽,而在Java中解決CAS的ABA問題主要有兩種方案:
- AtomicStampedReference:時間戳控制政钟,能夠完全解決
- AtomicMarkableReference:維護(hù)boolean值控制,不能完全杜絕
4.2.1、AtomicStampedReference
AtomicStampedReference是一個帶有時間戳的對象引用养交,內(nèi)部通過包裝Pair對象鍵值對的形式來存儲數(shù)據(jù)與時間戳衷戈,在每次更新時,先對數(shù)據(jù)本身和時間戳進(jìn)行比對层坠,只有當(dāng)兩者都符合預(yù)期值時才調(diào)用Unsafe的compareAndSwapObject方法進(jìn)行寫入殖妇。當(dāng)然,AtomicStampedReference不僅會設(shè)置新值而且還會記錄更改的時間戳破花。這也就解決了之前CAS機(jī)制帶來的ABA問題谦趣。簡單案例如下:
public class ABAIssue {
// 定義原子計(jì)數(shù)器,初始值 = 100
private static AtomicInteger atomicI = new AtomicInteger(100);
// 定義AtomicStampedReference:初始化時需要傳入一個初始值和初始時間
private static AtomicStampedReference<Integer> asRef = new AtomicStampedReference<Integer>(100, 0);
/**
* 未使用AtomicStampedReference線程組:TA TB
*/
private static Thread TA = new Thread(() -> {
System.err.println("未使用AtomicStampedReference線程組:[TA TB] >>>>");
// 更新值為101
boolean flag = atomicI.compareAndSet(100, 101);
System.out.println("線程TA:100 -> 101.... flag:" + flag + ",atomicINewValue:" + atomicI.get());
// 更新值為100
flag = atomicI.compareAndSet(101, 100);
System.out.println("線程TA:101 -> 100.... flag:" + flag + ",atomicINewValue:" + atomicI.get());
});
private static Thread TB = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean flag = atomicI.compareAndSet(100, 888);
System.out.println("線程TB:100 -> 888.... flag:" + flag + ",atomicINewValue:" + atomicI.get() + "\n\n");
});
/**
* 使用AtomicStampedReference線程組:T1 T2
*/
private static Thread T1 = new Thread(() -> {
System.err.println("使用AtomicStampedReference線程組:[T1 T2] >>>>");
// 更新值為101
boolean flag = asRef.compareAndSet(100, 101, asRef.getStamp(), asRef.getStamp() + 1);
System.out.println("線程T1:100 -> 101.... flag:" + flag + ".... asRefNewValue:" + asRef.getReference() + ".... 當(dāng)前Time:" + asRef.getStamp());
// 更新值為100
flag = asRef.compareAndSet(101, 100, asRef.getStamp(), asRef.getStamp() + 1);
System.out.println("線程T1:101 -> 100.... flag:" + flag + ".... asRefNewValue:" + asRef.getReference() + ".... 當(dāng)前Time:" + asRef.getStamp());
});
private static Thread T2 = new Thread(() -> {
int time = asRef.getStamp();
System.out.println("線程休眠前Time值:" + time);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean flag = asRef.compareAndSet(100, 888, time, time + 1);
System.out.println("線程T2:100 -> 888.... flag:" + flag + ".... asRefNewValue:" + asRef.getReference() + ".... 當(dāng)前Time:" + asRef.getStamp());
});
public static void main(String[] args) throws InterruptedException {
TA.start();
TB.start();
TA.join();
TB.join();
T1.start();
T2.start();
}
}
/**
* 未使用AtomicStampedReference線程組:[TA TB] >>>>
* 線程TA:100 -> 101.... flag:true,atomicINewValue:101
* 線程TA:101 -> 100.... flag:true,atomicINewValue:100
* 線程TB:100 -> 888.... flag:true,atomicINewValue:888
*
*
* 使用AtomicStampedReference線程組:[T1 T2] >>>>
* 線程休眠前Time值:0
* 線程T1:100 -> 101.... flag:true.... asRefNewValue:101.... 當(dāng)前Time:1
* 線程T1:101 -> 100.... flag:true.... asRefNewValue:100.... 當(dāng)前Time:2
* 線程T2:100 -> 888.... flag:false.... asRefNewValue:100.... 當(dāng)前Time:2
*/
我們觀察如上Demo中AtomicInteger與AtomicStampedReference的測試結(jié)果可以得知座每,AtomicStampedReference確實(shí)能夠解決我們在之前闡述的ABA問題前鹅,那么AtomicStampedReference究竟是如何ABA問題的呢?我們接下來再看看它的內(nèi)部實(shí)現(xiàn):
public class AtomicStampedReference<V> {
// 通過Pair內(nèi)部類存儲數(shù)據(jù)和時間戳
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
// 存儲數(shù)值和時間的內(nèi)部類
private volatile Pair<V> pair;
// 構(gòu)造方法:初始化時需傳入初始值和時間初始值
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
}
compareAndSet方法源碼實(shí)現(xiàn):
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
// 最終實(shí)現(xiàn)調(diào)用Unfase類中的compareAndSwapObject()方法
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
在compareAndSet方法源碼實(shí)現(xiàn)中我們不難發(fā)現(xiàn)峭梳,它其中同時對當(dāng)前數(shù)據(jù)和當(dāng)前時間進(jìn)行比較舰绘,只有兩者都相等是才會執(zhí)行casPair()方法,而casPair()方法也是一個原子性方法葱椭,最終實(shí)現(xiàn)調(diào)用的還是Unsafe類中的compareAndSwapObject()方法捂寿。
4.2.2、AtomicMarkableReference
AtomicMarkableReference與我們之前所探討的AtomicStampedReference并不相同孵运,AtomicMarkableReference只能在一定程度上減少ABA問題的出現(xiàn)秦陋,它并不能完全的杜絕ABA問題。因?yàn)锳tomicMarkableReference內(nèi)部維護(hù)的是boolean類型的標(biāo)識治笨,也就代表著它并不像之前的AtomicStampedReference內(nèi)部的維護(hù)的時間戳一樣值會不斷的遞增驳概,而AtomicMarkableReference內(nèi)部因?yàn)榫S護(hù)的是boolean,也就代表著只會在true與false兩種狀態(tài)之間來回切換旷赖,所以還是存在ABA問題出現(xiàn)的概念顺又。先來個demo感受一下:
public class ABAIssue {
static AtomicMarkableReference<Integer> amRef = new AtomicMarkableReference<Integer>(100, false);
private static Thread t1 = new Thread(() -> {
boolean mark = amRef.isMarked();
System.out.println("線程T1:修改前標(biāo)志 Mrak:" + mark + "....");
// 將值更新為200
System.out.println("線程T1:100 --> 200.... 修改后返回值 Result:" + amRef.compareAndSet(amRef.getReference(), 200, mark, !mark));
});
private static Thread t2 = new Thread(() -> {
boolean mark = amRef.isMarked();
System.out.println("線程T2:修改前標(biāo)志 Mrak:" + mark + "....");
// 將值更新回100
System.out.println("線程T2:200 --> 100.... 修改后返回值 Result:" + amRef.compareAndSet(amRef.getReference(), 100, mark, !mark));
});
private static Thread t3 = new Thread(() -> {
boolean mark = amRef.isMarked();
System.out.println("線程T3:休眠前標(biāo)志 Mrak:" + mark + "....");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean flag = amRef.compareAndSet(100, 500, mark, !mark);
System.out.println("線程T3: 100 --> 500.... flag:" + flag + ",newValue:" + amRef.getReference());
});
public static void main(String[] args) throws InterruptedException {
t1.start();
t1.join();
t2.start();
t2.join();
t3.start();
/**
* 輸出結(jié)果如下:
* 線程T1:修改前標(biāo)志 Mrak:false....
* 線程T1:100 --> 200.... 修改后返回值 Result:true
* 線程T2:修改前標(biāo)志 Mrak:true....
* 線程T2:200 --> 100.... 修改后返回值 Result:true
* 線程T3:休眠前標(biāo)志 Mrak:false....
* 線程T3: 100 --> 500.... flag:true,newValue:500
*/
/* t3線程執(zhí)行完成后結(jié)果還是成功更新為500,代表t1等孵、t2
線程所做的修改操作對于t3線程來說還是不可見的 */
}
}
從如上demo中我們可以發(fā)現(xiàn)稚照,其實(shí)使用AtomicMarkableReference后任然出現(xiàn)了ABA問題,并沒有真正的去避免了ABA問題的發(fā)生流济,那么關(guān)于AtomicMarkableReference的原理我們也不再闡述(大致和前面分析的AtomicStampedReference類似锐锣,有興趣的小伙伴可以自行研究~)腌闯。
我們從前面對于ABA問題的討論得知绳瘟,AtomicMarkableReference能夠在一定程度上減少ABA問題出現(xiàn)的幾率,但是無法做到完全的杜絕姿骏,而如果我們需要徹底避免ABA問題的出現(xiàn)還是需要使用AtomicStampedReference糖声。
五、再談CAS無鎖自旋
在本文開頭我們曾談到過無鎖的初步概念,其實(shí)無鎖的概念在我們上一篇文章:Synchronized關(guān)鍵字實(shí)現(xiàn)原理剖析鎖升級膨脹過程中也多次提到蘸泻。而無鎖也在有些地方被稱作自旋鎖琉苇,自旋是一種如果出現(xiàn)線程競爭,但是此處的邏輯線程執(zhí)行起來非吃檬快并扇,某些沒有獲取到資源(數(shù)值等)的線程也能在不久后的將來獲取到資源并執(zhí)行時,OS讓沒有獲取到資源的線程執(zhí)行幾個空循環(huán)的操作等待資源的情況抡诞。而自旋這種操作也的確可以提升效率穷蛹,因?yàn)樽孕i只是在邏輯上阻塞了線程,在用戶態(tài)的情況下讓線程停止了執(zhí)行昼汗,并沒有真正意義上在內(nèi)核態(tài)中掛起對應(yīng)的內(nèi)核線程肴熏,從而可以減少很多內(nèi)核掛起/放下線程耗費(fèi)的資源。但問題是當(dāng)線程越來越多競爭很激烈時顷窒,占用CPU的時間變長會導(dǎo)致性能急劇下降蛙吏,因此Java虛擬機(jī)內(nèi)部一般對于自旋鎖有一定的次數(shù)限制,可能是50或者100次循環(huán)后就放棄鞋吉,直接讓OS掛起內(nèi)核線程鸦做,讓出CPU資源。
六谓着、參考資料與書籍
- 《深入理解JVM虛擬機(jī)》
- 《Java并發(fā)編程之美》
- 《Java高并發(fā)程序設(shè)計(jì)》
- 《億級流量網(wǎng)站架構(gòu)核心技術(shù)》
- 《Java并發(fā)編程實(shí)戰(zhàn)》