原文鏈接:Java并發(fā)編程-無鎖CAS與Unsafe類及其并發(fā)包Atomic - CSDN博客
在前面一篇博文中捺宗,我們曾經(jīng)詳談過有鎖并發(fā)的典型代表synchronized關(guān)鍵字涛贯,通過該關(guān)鍵字可以控制并發(fā)執(zhí)行過程中有且只有一個線程可以訪問共享資源,其原理是通過當(dāng)前線程持有當(dāng)前對象鎖钞钙,從而擁有訪問權(quán)限熄浓,而其他沒有持有當(dāng)前對象鎖的線程無法擁有訪問權(quán)限校坑,也就保證了線程安全闯团。但在本篇中,我們將會詳聊另外一種反向而行的并發(fā)策略疹味,即無鎖并發(fā)仅叫,即不加鎖也能保證并發(fā)執(zhí)行的安全性。?
本篇的思路是先闡明無鎖執(zhí)行者CAS的核心算法原理然后分析Java執(zhí)行CAS的實(shí)踐者Unsafe類糙捺,該類中的方法都是native修飾的诫咱,因此我們會以說明方法作用為主介紹Unsafe類,最后再介紹并發(fā)包中的Atomic系統(tǒng)使用CAS原理實(shí)現(xiàn)的并發(fā)類洪灯。
無鎖的概念
在談?wù)摕o鎖概念時坎缭,總會關(guān)聯(lián)起樂觀派與悲觀派,對于樂觀派而言签钩,他們認(rèn)為事情總會往好的方向發(fā)展掏呼,總是認(rèn)為壞的情況發(fā)生的概率特別小,可以無所顧忌地做事铅檩,但對于悲觀派而已憎夷,他們總會認(rèn)為發(fā)展事態(tài)如果不及時控制,以后就無法挽回了昧旨,即使無法挽回的局面幾乎不可能發(fā)生拾给。這兩種派系映射到并發(fā)編程中就如同加鎖與無鎖的策略祥得,即加鎖是一種悲觀策略,無鎖是一種樂觀策略蒋得,因?yàn)閷τ诩渔i的并發(fā)程序來說级及,它們總是認(rèn)為每次訪問共享資源時總會發(fā)生沖突,因此必須對每一次數(shù)據(jù)操作實(shí)施加鎖策略额衙。而無鎖則總是假設(shè)對共享資源的訪問沒有沖突饮焦,線程可以不停執(zhí)行,無需加鎖窍侧,無需等待县踢,一旦發(fā)現(xiàn)沖突,無鎖策略則采用一種稱為CAS的技術(shù)來保證線程執(zhí)行的安全性疏之,這項(xiàng)CAS技術(shù)就是無鎖策略實(shí)現(xiàn)的關(guān)鍵,下面我們進(jìn)一步了解CAS技術(shù)的奇妙之處暇咆。
無鎖的執(zhí)行者-CAS
CAS
CAS的全稱是Compare And Swap 即比較交換锋爪,其算法核心思想如下:
執(zhí)行函數(shù):CAS(V,E,N)
其包含3個參數(shù)
1. V表示要更新的變量
2. E表示預(yù)期值
3. N表示新值
如果V值等于E值,則將V的值設(shè)為N爸业。若V值和E值不同其骄,則說明已經(jīng)有其他線程做了更新,則當(dāng)前線程什么都不做扯旷。通俗的理解就是CAS操作需要我們提供一個期望值拯爽,當(dāng)期望值與當(dāng)前線程的變量值相同時,說明還沒線程修改該值钧忽,當(dāng)前線程可以進(jìn)行修改毯炮,也就是執(zhí)行CAS操作,但如果期望值與當(dāng)前線程不符耸黑,則說明該值已被其他線程修改桃煎,此時不執(zhí)行更新操作,但可以選擇重新讀取該變量再嘗試再次修改該變量大刊,也可以放棄操作为迈,原理圖如下:
由于CAS操作屬于樂觀派,它總認(rèn)為自己可以成功完成操作缺菌,當(dāng)多個線程同時使用CAS操作一個變量時葫辐,只有一個會勝出,并成功更新伴郁,其余均會失敗耿战,但失敗的線程并不會被掛起,僅是被告知失敗焊傅,并且允許再次嘗試昆箕,當(dāng)然也允許失敗的線程放棄操作鸦列,這點(diǎn)從圖中也可以看出來∨籼龋基于這樣的原理薯嗤,CAS操作即使沒有鎖,同樣知道其他線程對共享資源操作影響纤泵,并執(zhí)行相應(yīng)的處理措施骆姐。同時從這點(diǎn)也可以看出,由于無鎖操作中沒有鎖的存在捏题,因此不可能出現(xiàn)死鎖的情況玻褪,也就是說無鎖操作天生免疫死鎖。
CPU指令對CAS的支持
或許我們可能會有這樣的疑問公荧,假設(shè)存在多個線程執(zhí)行CAS操作并且CAS的步驟很多带射,有沒有可能在判斷V和E相同后,正要賦值時循狰,切換了線程窟社,更改了值。造成了數(shù)據(jù)不一致呢绪钥?答案是否定的灿里,因?yàn)镃AS是一種系統(tǒng)原語,原語屬于操作系統(tǒng)用語范疇程腹,是由若干條指令組成的匣吊,用于完成某個功能的一個過程,并且原語的執(zhí)行必須是連續(xù)的寸潦,在執(zhí)行過程中不允許被中斷色鸳,也就是說CAS是一條CPU的原子指令,不會造成所謂的數(shù)據(jù)不一致問題见转。
鮮為人知的指針: Unsafe類
Unsafe類存在于sun.misc包中缕碎,其內(nèi)部方法操作可以像C的指針一樣直接操作內(nèi)存,單從名稱看來就可以知道該類是非安全的池户,畢竟Unsafe擁有著類似于C的指針操作咏雌,因此總是不應(yīng)該首先使用Unsafe類,Java官方也不建議直接使用的Unsafe類校焦,據(jù)說Oracle正在計(jì)劃從Java 9中去掉Unsafe類赊抖,但我們還是很有必要了解該類,因?yàn)镴ava中CAS操作的執(zhí)行依賴于Unsafe類的方法寨典,注意Unsafe類中的所有方法都是native修飾的氛雪,也就是說Unsafe類中的方法都直接調(diào)用操作系統(tǒng)底層資源執(zhí)行相應(yīng)任務(wù),關(guān)于Unsafe類的主要功能點(diǎn)如下:
內(nèi)存管理耸成,Unsafe類中存在直接操作內(nèi)存的方法
//分配內(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();
提供實(shí)例對象新途徑报亩。
//傳入一個對象的class并創(chuàng)建該實(shí)例對象浴鸿,但不會調(diào)用構(gòu)造方法
public native Object allocateInstance(Class cls) throws InstantiationException;
類和實(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類的理解:
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)建對象
? ? ? ? User user = (User) unsafe.allocateInstance(User.class);
? ? ? ? Class userClass = user.getClass();
? ? ? ? Field name = userClass.getDeclaredField("name");
? ? ? ? Field age = userClass.getDeclaredField("age");
? ? ? ? Field id = userClass.getDeclaredField("id");
? ? ? ? //獲取實(shí)例變量name和age在對象內(nèi)存中的偏移量并設(shè)置值
? ? ? ? unsafe.putInt(user,unsafe.objectFieldOffset(age),18);
? ? ? ? unsafe.putObject(user,unsafe.objectFieldOffset(name),"android TV");
? ? ? ? // 這里返回 User.class择浊,
? ? ? ? Object staticBase = unsafe.staticFieldBase(id);
? ? ? ? System.out.println("staticBase:"+staticBase);
? ? ? ? //獲取靜態(tài)變量id的偏移量staticOffset
? ? ? ? long staticOffset = unsafe.staticFieldOffset(userClass.getDeclaredField("id"));
? ? ? ? //獲取靜態(tài)變量的值
? ? ? ? System.out.println("設(shè)置前的ID:"+unsafe.getObject(staticBase,staticOffset));
? ? ? ? //設(shè)置值
? ? ? ? unsafe.putObject(staticBase,staticOffset,"SSSSSSSS");
? ? ? ? //獲取靜態(tài)變量的值
? ? ? ? System.out.println("設(shè)置前的ID:"+unsafe.getObject(staticBase,staticOffset));
? ? ? ? //輸出USER
? ? ? ? System.out.println("輸出USER:"+user.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@6f94fa3e
? ? ? ? staticBase:class geym.conc.ch4.atomic.User
? ? ? ? 設(shè)置前的ID:USER_ID
? ? ? ? 設(shè)置前的ID:SSSSSSSS
? ? ? ? 輸出USER:User{name='android TV', age=18', id=SSSSSSSS'}
? ? ? ? addrData:1000
? ? ? ? */? ? }
}
class User{
? ? public User(){
? ? ? ? System.out.println("user 構(gòu)造方法被調(diào)用");
? ? }
? ? private String name;
? ? private int age;
? ? private static String id="USER_ID";
? ? @Override
? ? public String toString() {
? ? ? ? return "User{" +
? ? ? ? ? ? ? ? "name='" + name + '\'' +
? ? ? ? ? ? ? ? ", age=" + age +'\'' +
? ? ? ? ? ? ? ? ", id=" + id +'\'' +
? ? ? ? ? ? ? ? '}';
? ? }
}
雖然在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;
? }
數(shù)組操作
//獲取數(shù)組第一個元素的偏移地址
public native int arrayBaseOffset(Class arrayClass);
//數(shù)組中一個元素占據(jù)的內(nèi)存空間,arrayBaseOffset與arrayIndexScale配合使用膳帕,可定位數(shù)組中每個元素在內(nèi)存中的位置
public native int arrayIndexScale(Class arrayClass);
CAS 操作相關(guān)?
CAS是一些CPU直接支持的指令,也就是我們前面分析的無鎖操作薇缅,在Java中無鎖操作CAS基于以下3個方法實(shí)現(xiàn)危彩,在稍后講解Atomic系列內(nèi)部方法是基于下述方法的實(shí)現(xiàn)的。
//第一個參數(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);
這里還需介紹Unsafe類中JDK 1.8新增的幾個方法浮毯,它們的實(shí)現(xiàn)是基于上述的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;
}
上述的方法我們在稍后的Atomic系列分析中還會見到它們的身影龙誊。
掛起與恢復(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()方法:
//線程調(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);
內(nèi)存屏障
這里主要包括了loadFence懈糯、storeFence涤妒、fullFence等方法,這些方法是在Java 8新引入的赚哗,用于定義內(nèi)存屏障她紫,避免代碼重排序,與Java內(nèi)存模型相關(guān)屿储,感興趣的可以看博主的另一篇博文全面理解Java內(nèi)存模型(JMM)及volatile關(guān)鍵字贿讹,這里就不展開了。
//在該方法之前的所有讀操作扩所,一定在load屏障之前執(zhí)行完成
public native void loadFence();
//在該方法之前的所有寫操作围详,一定在store屏障之前執(zhí)行完成
public native void storeFence();
//在該方法之前的所有讀寫操作,一定在full屏障之前執(zhí)行完成,這個內(nèi)存屏障相當(dāng)于上面兩個的合體功能
public native void fullFence();
其他操作
//獲取持有鎖助赞,已不建議使用
@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);